Ecto.Changeset - How do I make validation errors in embedded schemas show up on the parent?

Hi, I’m using Ecto embedded schemas for input validation of a JSON API. Basically, I have a Plug.Router implementation that accepts JSON and casts the input data into an Elixir struct with Ecto:

  ...
  post "/validate" do
    payload = conn.body_params

    changeset = MyApp.Document.changeset(%MyApp.Document{}, payload)
    if changeset.valid? do
      document = Ecto.Changeset.apply_changes(changeset)
      IO.inspect(document)
      send_resp(conn, 200, "Payload: #{inspect(document)}\n")
    else
      IO.inspect(changeset.errors)
      send_resp(conn, 400, "Error 400: Bad Request: #{inspect(changeset.errors)}\n")
    end
  end
  ...

My current implementation of the Ecto schema for MyApp.Document is shown below.
As you can see, a Document embeds a bunch of children, which for simplicity’s sake is now just Text elements.

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

  @primary_key false
  embedded_schema do
    field :id, Ecto.UUID
    field :type, :string
    embeds_many :children, MyApp.Text
  end

  def changeset(document, attrs) do
    document
    |> cast(attrs, [:id, :type])
    |> cast_embed(:children)
    |> validate_required([:id, :type])
    |> validate_inclusion(:type, ["https://spec.nldoc.nl/Resource/Document"])
  end
end

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

  @primary_key false
  embedded_schema do
    field :id, Ecto.UUID
    field :type, :string
    field :text, :string
  end

  def changeset(text, attrs) do
    text
    |> cast(attrs, [:id, :type, :text])
    |> validate_required([:id, :type, :text])
    |> validate_inclusion(:type, ["https://spec.nldoc.nl/Resource/Text"])
  end
end

The casting in my router works in the sense that if I supply a document with an invalid id or where the type is incorrect, then changeset.errors in my router contains an error message and validation fails as expected (so 400 is returned).

However, if I have a similar problem in the any of the embedded objects, then the validation does indeed fail as expected, but changeset.errors in my router is empty.

How do I ensure that any errors originating from casting / validating embedded schemas (MyApp.Text), end up on the changeset returned by the parent (MyApp.Document)? Or how else do I ensure that changeset.errors in my router contains all validation errors, even from nested embeds?

This is expected. There were no validation failures for the fields of the document. The validation values were on the children and their nested child changesets have errors.

You can iterate the nested errors with: Ecto.Changeset — Ecto v3.12.1

Thanks for your quick reply!

That makes sense, but I don’t quite understand yet how I can use traverse_errors to move the errors from the nested child changesets onto the parent’s changeset. The example shown in the docs doesn’t show how it can be used for assocs and embeds, it looks more like after-the-fact error message customisation.

Could you please show me an example of its usage with embedded schemas?

i also don’t really understand where the children’s errors are stored, as I don’t see them in any of the intermediate changesets between all function calls in MyApp.Document.changeset

You generally don’t do that. The errors of children are not meant to go on the key of the parent changeset.

They’re in parent_changeset.changes.children[*].errors. The children under changes are also changeset structs, where each has their own errors.

It’s not just that. It also traverses all the nested changesets within the parent and allows you to build a nested structure of all their errors. The result is likely what you want to return instead of changeset.errors.

Indeed, I want to build a nested structure of the errors, so that I may return to my callers that e.g. document.children[2].children[0].text is missing or something like that.

However, traverse_errors is typed such that only strings should be returned, so I’m not sure if it suits my use-case exactly.

With this knowledge, though, I could definitely put together some function to recursively iterate through all the errors and make my nested structure that way.

Thanks! I’ll mark your answer as solution for future readers :slight_smile:

Sadly, there is one thing preventing me from being able to solve this problem at the moment, namely that my real code makes use of PolymorphicEmbed which apparently only places an {"is invalid", []} error under changeset.errors[:children] when there’s something wrong in any of the children.

I’m eager to hear from @mathieuprog, the owner of PolymorphicEmbed, what his opinion is on this issue and what kind of solution would be in order.

Did you look into this?

2 Likes

Hi, thanks for your quick reply! Yes, I did find PolymorphicEmbed.traverse_errors/2 and I tried it (I even examined its implementation), but it can’t iterate over errors in a child changeset that doesn’t exist :sweat_smile:

I’ve created a minimal working example in a GitHub repo here just to illustrate the point a bit better:

See lib/schema.ex and test/schema_test.exs to see what I mean. I’ve added some tests that show the current behaviour and the behaviour that would be desirable (and that would be more akin to what Ecto does for embeds_many).

Do you want to continue this conversation here, or should I make an issue on the PolymorphicEmbed repo and continue there?

1 Like