You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用无模式Ecto.changeset验证嵌套JSON并完整存入数据库?

嘿,我正好处理过类似的场景,给你几个实用的解决方案,你可以根据自己的需求选:

1. 用Ecto Schema + Map字段(简单直接)

如果不想拆分数据,但又想利用Ecto Changeset的验证能力,最直接的方式是在Schema里定义一个:map类型的字段,然后添加自定义验证函数检查嵌套JSON的结构。

示例代码:

defmodule MyApp.ComplexPayload do
  use Ecto.Schema
  import Ecto.Changeset

  schema "complex_payloads" do
    field :data, :map  # 整个嵌套JSON存在这个字段里
    timestamps()
  end

  def changeset(payload, attrs) do
    payload
    |> cast(attrs, [:data])
    |> validate_required([:data])
    |> validate_nested_structure()  # 自定义验证嵌套结构的函数
  end

  defp validate_nested_structure(changeset) do
    data = get_field(changeset, :data)

    cond do
      not is_map(data) ->
        add_error(changeset, :data, "必须是有效的JSON对象")

      not Map.has_key?(data, "user") or not is_map(data["user"]) ->
        add_error(changeset, :data, "缺少必填的user嵌套对象")

      is_nil(data["user"]["id"]) or not is_integer(data["user"]["id"]) ->
        add_error(changeset, :data, "user.id必须是整数类型")

      is_nil(data["user"]["profile"]) or not is_map(data["user"]["profile"]) ->
        add_error(changeset, :data, "user.profile必须是嵌套对象")

      true ->
        changeset  # 所有验证通过
    end
  end
end

这种方式的好处是不需要额外定义嵌套Schema,适合结构不算特别复杂的场景,验证逻辑完全由你自己控制。

2. 嵌入式Schema(优雅的结构化验证)

如果嵌套JSON的结构是固定的,嵌入式Schema是最佳选择——它既能让你用Ecto内置的验证规则(比如validate_required、validate_format),又会把整个嵌套结构作为一个完整的Map存在数据库里,不会拆分到其他表。

示例代码:

# 定义嵌套的嵌入式Schema
defmodule MyApp.Embedded.UserProfile do
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :bio, :string
    field :avatar_url, :string
    field :age, :integer
  end

  def changeset(profile, attrs) do
    profile
    |> cast(attrs, [:bio, :avatar_url, :age])
    |> validate_required([:bio])
    |> validate_number(:age, greater_than: 18)
  end
end

defmodule MyApp.Embedded.User do
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :id, :integer
    field :name, :string
    field :email, :string
    embeds_one :profile, MyApp.Embedded.UserProfile  # 支持多层嵌套
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:id, :name, :email])
    |> validate_required([:id, :name, :email])
    |> validate_format(:email, ~r/@/)
    |> cast_embed(:profile, required: true)  # 自动验证嵌套的Profile结构
  end
end

# 主Schema
defmodule MyApp.ComplexPayload do
  use Ecto.Schema
  import Ecto.Changeset

  schema "complex_payloads" do
    embeds_one :data, MyApp.Embedded.User  # 用embeds_one存储整个嵌套结构
    timestamps()
  end

  def changeset(payload, attrs) do
    payload
    |> cast(attrs, [])
    |> cast_embed(:data, required: true)  # 一键触发所有嵌套验证
  end
end

使用这种方式时,Ecto会自动帮你处理嵌套结构的验证,错误信息也会精准定位到具体的嵌套字段,非常适合结构复杂且固定的场景。

3. 无模式Changeset(完全灵活)

如果连主Schema都不想定义,也可以直接用无模式的Changeset来验证。之前你尝试没成功可能是写法问题,下面是正确的示例:

def validate_complex_json(params) do
  # 以空Map为基础构建Changeset
  %{}
  |> Ecto.Changeset.cast(params, [:data])
  |> Ecto.Changeset.validate_required([:data])
  |> Ecto.Changeset.validate_change(:data, fn :data, data ->
    errors = []

    # 检查顶层结构
    unless is_map(data) do
      errors = [{:data, "必须是JSON对象"} | errors]
    end

    # 检查user字段
    if is_map(data) do
      user = Map.get(data, "user")
      unless is_map(user) do
        errors = [{:data, "user必须是嵌套对象"} | errors]
      else
        # 检查user的子字段
        unless is_integer(Map.get(user, "id")) do
          errors = [{:data, "user.id必须是整数"} | errors]
        end
        unless is_binary(Map.get(user, "name")) do
          errors = [{:data, "user.name必须是字符串"} | errors]
        end
      end
    end

    errors
  end)
end

# 使用示例
attrs = %{"data" => %{"user" => %{"id" => 123, "name" => "Alan"}}}
changeset = validate_complex_json(attrs)

if changeset.valid? do
  # 直接把valid的data存入数据库,比如:
  # Repo.insert(%MyApp.ComplexPayload{data: changeset.changes.data})
else
  IO.inspect(changeset.errors)  # 查看验证错误
end

这种方式完全脱离Schema,适合临时的、结构不固定的验证场景,但需要手动编写所有验证逻辑,维护成本较高。


内容的提问来源于stack exchange,提问作者Alan Alves de Oliveira

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 09:34:16