elixir-ecto / ecto

A toolkit for data mapping and language integrated query.
https://hexdocs.pm/ecto
Apache License 2.0
6.15k stars 1.43k forks source link

Changeset.traverse_errors/2 not traversing embedded schemas #4493

Closed kevinkirkup closed 1 month ago

kevinkirkup commented 1 month ago

Elixir version

elixir 1.17.2-otp-27

Database and Version

Postgres 11

Ecto Versions

ecto_sql 3.11.2, ecto 3.11.2

Database Adapter and Versions (postgrex, myxql, etc)

Postgres 0.19.1

Current behavior

changeset #=> #Ecto.Changeset<
  action: nil,
  changes: %{
    name: "Testy-Connection-J5fYanBereAvND6feBjAjE",
    description: "some description",
    interface_configuration: #Ecto.Changeset<
      action: :insert,
      changes: %{
        interface: #Ecto.Changeset<
          action: :insert,
          changes: %{
            nested_config: [
              #Ecto.Changeset<
                action: :insert,
                changes: %{
                  something: #Ecto.Changeset<
                    action: :insert,
                    changes: %{value1: 10, value2: 10},
                    errors: [
                      enable: {"can't be blank", [validation: :required]}
                    ],
                    data: #SomeModule.Connections.SomeConfiguration<>,
                    valid?: false
                  >,
                  some_value: 12345,
                },
                errors: [],
                data: #SomeModule.Connections.NestedConfiguration<>,
                valid?: false
              >
            ]
          },
          errors: [],
          data: #SomeModule.Connections.Interface<>,
          valid?: false
        >,
        standard: :blah
      },
      errors: [],
      data: #SomeModule.Connections.NestedInterfaceConfiguration<>,
      valid?: false
    >,
    random_id: "uc3oG7ymJhvmFri2eYZ8BW",
  },
  errors: [],
  data: #SomeModule.Connections.Connection<>,
  valid?: false
>
|> Changeset.traverse_errors(fn {msg, _opts} -> msg end) #=> %{}

Expected behavior

|> Changeset.traverse_errors(fn {msg, _opts} -> msg end) #=> %{
  {:interface_configuration, {
    :interface, {
      :nested_config, {
        :enable: {"can't be blank", [validation: :required]}
      }
    }
  }
}
kevinkirkup commented 1 month ago

The valid? flag is being propagated properly but the the errors for the nested changesets are not.

josevalim commented 1 month ago

Can you please provide a small snippet that reproduces the error? You can use Mix.install([:ecto]) and build your schemas there. Thank you.

jdanielnd commented 1 month ago

I tried to reproduce the error without success using the script below.

Mix.install([
  {:ecto, "~> 3.11.2"}
])

defmodule User do
  use Ecto.Schema

  import Ecto.Changeset

  schema "users" do
    field :name, :string
    field :age, :integer
    embeds_many :posts, Post
  end

  def changeset(model, params \\ %{}) do
    model
    |> cast(params, [:name, :age])
    |> cast_embed(:posts, with: &Post.changeset/2)
    |> validate_length(:name, min: 2)
    |> validate_inclusion(:age, 18..100)
  end
end

defmodule Post do
  use Ecto.Schema

  import Ecto.Changeset

  schema "posts" do
    field :title, :string
    field :content, :string
    belongs_to :user, User
    embeds_many :comments, Comment
  end

  def changeset(model, params \\ %{}) do
    model
    |> cast(params, [:title, :content])
    |> cast_embed(:comments, with: &Comment.changeset/2)
    |> validate_required([:title, :content])
    |> validate_length(:title, min: 2)
  end
end

defmodule Comment do
  use Ecto.Schema

  import Ecto.Changeset

  schema "comments" do
    field :content, :string
    belongs_to :post, Post
  end

  def changeset(model, params \\ %{}) do
    model
    |> cast(params, [:content])
    |> validate_required([:content])
    |> validate_length(:content, min: 2)
  end
end

# Changeset with errors in all levels
changeset = User.changeset(%User{}, %{
  name: "J",
  age: 16,
  posts: [
    %{
      title: "A",
      content: "B",
      comments: [
        %{content: "C"},
        %{content: "D"}
      ]
    }
  ]
})

Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)

=> %{
  name: ["should be at least %{count} character(s)"],
  age: ["is invalid"],
  posts: [
    %{
      title: ["should be at least %{count} character(s)"],
      comments: [
        %{content: ["should be at least %{count} character(s)"]},
        %{content: ["should be at least %{count} character(s)"]}
      ]
    }
  ]
}

# Changeset with errors in the deepest level
changeset = User.changeset(%User{}, %{
  name: "John",
  age: 34,
  posts: [
    %{
      title: "A very long title",
      content: "Beautiful content",
      comments: [
        %{content: "Comment 1"},
        %{content: nil}
      ]
    }
  ]
})

Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)

=> %{posts: [%{comments: [%{}, %{content: ["can't be blank"]}]}]}
josevalim commented 1 month ago

Thank you, we can close this for now and reopen once we can reproduce it!

kevinkirkup commented 1 month ago

Just a heads up. This looks like it was related to an issue with the https://hexdocs.pm/polymorphic_embed module, not with Ecto itself.

https://github.com/mathieuprog/polymorphic_embed/blob/324b73108320442cbc34eea2736833e1c5b37e4e/README.md?plain=1#L267

Thanks for looking!