How can I show all validation errors (including nested) at once?

I'm using manage_relationship/4 in a top-level action that accepts nested fields via arguments. Right now, the nested validation errors only show after the top-level data is valid. Is there a way to surface all validation errors (parent + nested) simultaneously? I'd like users to see everything in one go
Solution:
If you have time to make a PR, what needs to happen is something like this: ```elixir form |> AshPhoenix.Form.raw_errors(for_path: :all)...
Jump to solution
17 Replies
ZachDaniel
ZachDaniel3mo ago
🤔 it should show validation errors for all of them, that might be a bug How are you setting up your forms? Might need a reproduction for this one
Joan Gavelán
Joan GavelánOP3mo ago
# user.ex
defmodule Culturalismo.Accounts.User do
use Ash.Resource,
otp_app: :culturalismo,
data_layer: AshPostgres.DataLayer

actions do
create :register_with_password do
argument :email, :ci_string, allow_nil?: false
argument :password, :string, allow_nil?: false, constraints: [min_length: 6]
argument :profile, :map, allow_nil?: false

change set_attribute(:email, arg(:email))
change manage_relationship(:profile,
type: :create,
error_path: :profile
)
change AshAuthentication.Strategy.Password.HashPasswordChange
change AshAuthentication.GenerateTokenChange
end
end

attributes do
uuid_v7_primary_key :id
attribute :email, :ci_string, allow_nil?: false
attribute :hashed_password, :string, allow_nil?: false
end

relationships do
has_one :profile, Culturalismo.Accounts.Profile, allow_nil?: false
end
end
# user.ex
defmodule Culturalismo.Accounts.User do
use Ash.Resource,
otp_app: :culturalismo,
data_layer: AshPostgres.DataLayer

actions do
create :register_with_password do
argument :email, :ci_string, allow_nil?: false
argument :password, :string, allow_nil?: false, constraints: [min_length: 6]
argument :profile, :map, allow_nil?: false

change set_attribute(:email, arg(:email))
change manage_relationship(:profile,
type: :create,
error_path: :profile
)
change AshAuthentication.Strategy.Password.HashPasswordChange
change AshAuthentication.GenerateTokenChange
end
end

attributes do
uuid_v7_primary_key :id
attribute :email, :ci_string, allow_nil?: false
attribute :hashed_password, :string, allow_nil?: false
end

relationships do
has_one :profile, Culturalismo.Accounts.Profile, allow_nil?: false
end
end
# profile.ex
defmodule Culturalismo.Accounts.Profile do
use Ash.Resource,
otp_app: :culturalismo,
data_layer: AshPostgres.DataLayer

actions do
create :create do
primary? true
accept [:first_name, :last_name]
change Culturalismo.Accounts.Profile.Changes.GenerateUsername, only_when_valid?: true
end
end

attributes do
attribute :first_name, :string, allow_nil?: false, constraints: [min_length: 2]
attribute :last_name, :string, allow_nil?: false, constraints: [min_length: 2]
attribute :username, :string, allow_nil?: false
end

relationships do
belongs_to :user, Culturalismo.Accounts.User, primary_key?: true, allow_nil?: false
end
end
# profile.ex
defmodule Culturalismo.Accounts.Profile do
use Ash.Resource,
otp_app: :culturalismo,
data_layer: AshPostgres.DataLayer

actions do
create :create do
primary? true
accept [:first_name, :last_name]
change Culturalismo.Accounts.Profile.Changes.GenerateUsername, only_when_valid?: true
end
end

attributes do
attribute :first_name, :string, allow_nil?: false, constraints: [min_length: 2]
attribute :last_name, :string, allow_nil?: false, constraints: [min_length: 2]
attribute :username, :string, allow_nil?: false
end

relationships do
belongs_to :user, Culturalismo.Accounts.User, primary_key?: true, allow_nil?: false
end
end
[info] POST /auth/register
[debug] Processing with CulturalismoWeb.UserRegistrationController.create/2
Parameters: %{"email" => "", "password" => "[FILTERED]", "profile" => %{"first_name" => "", "last_name" => ""}}
Pipelines: [:browser, :redirect_if_user_is_authenticated]
Params: %{
"email" => "",
"password" => "",
"profile" => %{"first_name" => "", "last_name" => ""}
}
%Ash.Error.Invalid{
bread_crumbs: ["Error returned from: Culturalismo.Accounts.User.register_with_password"],
changeset: "#Changeset<>",
errors: [
%Ash.Error.Changes.Required{
field: :email,
type: :argument,
resource: Culturalismo.Accounts.User,
splode: Ash.Error,
bread_crumbs: ["Error returned from: Culturalismo.Accounts.User.register_with_password"],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
},
%Ash.Error.Changes.Required{
field: :password,
type: :argument,
resource: Culturalismo.Accounts.User,
splode: Ash.Error,
bread_crumbs: ["Error returned from: Culturalismo.Accounts.User.register_with_password"],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
}
]
}
[info] POST /auth/register
[debug] Processing with CulturalismoWeb.UserRegistrationController.create/2
Parameters: %{"email" => "", "password" => "[FILTERED]", "profile" => %{"first_name" => "", "last_name" => ""}}
Pipelines: [:browser, :redirect_if_user_is_authenticated]
Params: %{
"email" => "",
"password" => "",
"profile" => %{"first_name" => "", "last_name" => ""}
}
%Ash.Error.Invalid{
bread_crumbs: ["Error returned from: Culturalismo.Accounts.User.register_with_password"],
changeset: "#Changeset<>",
errors: [
%Ash.Error.Changes.Required{
field: :email,
type: :argument,
resource: Culturalismo.Accounts.User,
splode: Ash.Error,
bread_crumbs: ["Error returned from: Culturalismo.Accounts.User.register_with_password"],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
},
%Ash.Error.Changes.Required{
field: :password,
type: :argument,
resource: Culturalismo.Accounts.User,
splode: Ash.Error,
bread_crumbs: ["Error returned from: Culturalismo.Accounts.User.register_with_password"],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
}
]
}
Hope that can help
ZachDaniel
ZachDaniel3mo ago
The errors on the top level of the form aren't all the errors AshPhoenix.Form.errors(form, for_path: :all)
Joan Gavelán
Joan GavelánOP3mo ago
I'm using Inertia, I'm just passing down the errors I receive from executing the action
def create(conn, params) do
case Accounts.register_with_password(params) do
{:ok, user} ->
conn
|> redirect(to: ~p"/auth/register")

{:error, error} ->
conn
|> assign_errors(error)
|> redirect(to: ~p"/auth/register")
end
end
def create(conn, params) do
case Accounts.register_with_password(params) do
{:ok, user} ->
conn
|> redirect(to: ~p"/auth/register")

{:error, error} ->
conn
|> assign_errors(error)
|> redirect(to: ~p"/auth/register")
end
end
ZachDaniel
ZachDaniel3mo ago
Ah, okay so that may be an issue with our protocol implementation then Please open a bug on ash_phoenix
Joan Gavelán
Joan GavelánOP3mo ago
GitHub
Nested validation errors only show after parent is valid · Issue #...
Code of Conduct I agree to follow this project&#39;s Code of Conduct AI Policy I agree to follow this project&#39;s AI Policy, or I agree that AI was not used while creating this issue. Versions Er...
ZachDaniel
ZachDaniel3mo ago
assign_errors is the inertia helper right?
ZachDaniel
ZachDaniel3mo ago
GitHub
ash_phoenix/lib/ash_phoenix/inertia/error.ex at v2.3.12 · ash-proj...
Utilities for integrating Ash and Phoenix. Contribute to ash-project/ash_phoenix development by creating an account on GitHub.
Solution
ZachDaniel
ZachDaniel3mo ago
If you have time to make a PR, what needs to happen is something like this:
form
|> AshPhoenix.Form.raw_errors(for_path: :all)
|> Enum.flat_map(fn {path, errors} ->
Enum.map(errors, &Ash.Error.set_path(&1, path))
end)
|> to_errors(message_func)
form
|> AshPhoenix.Form.raw_errors(for_path: :all)
|> Enum.flat_map(fn {path, errors} ->
Enum.map(errors, &Ash.Error.set_path(&1, path))
end)
|> to_errors(message_func)
Joan Gavelán
Joan GavelánOP3mo ago
That's right
ZachDaniel
ZachDaniel3mo ago
If you don't have time to make a PR mind copying my comments just now? I'm mega slammed rn like copying into hte issue you opened I mean not sure when I'll get to the fix
Joan Gavelán
Joan GavelánOP3mo ago
Happy to open my first PR but I'm a bit confused about the solution, where is the form variable coming from? Should I simply paste the code you provided? I'm a bit of a newbie to all this 😅
ZachDaniel
ZachDaniel3mo ago
It's that first argument to that function I linked We're pattern matching just the top level errors But yeah pretty much paste that code in there and change the pattern matched var to just "form" Feel free to get it started and if you need help I can help on your PR 🙂
Joan Gavelán
Joan GavelánOP3mo ago
Okay I'll get to it as soon as I can, probably in a couple of hours. I'll let you know! Hey Zach. I'm working on it, I modified the function at line 88
def to_errors(form, message_func) do
form
|> AshPhoenix.Form.raw_errors(for_path: :all)
|> Enum.flat_map(fn {path, errors} ->
Enum.map(errors, &Ash.Error.set_path(&1, path))
end)
|> to_errors(message_func)
end
def to_errors(form, message_func) do
form
|> AshPhoenix.Form.raw_errors(for_path: :all)
|> Enum.flat_map(fn {path, errors} ->
Enum.map(errors, &Ash.Error.set_path(&1, path))
end)
|> to_errors(message_func)
end
There is a problem though:
mix test
Compiling 1 file (.ex)
warning: this clause for to_errors/2 cannot match because a previous clause at line 88 always matches

97 │ def to_errors(error_or_errors, message_func) do
│ ~

└─ lib/ash_phoenix/inertia/error.ex:97:9

Generated ash_phoenix app
Running ExUnit with seed: 514125, max_cases: 24
mix test
Compiling 1 file (.ex)
warning: this clause for to_errors/2 cannot match because a previous clause at line 88 always matches

97 │ def to_errors(error_or_errors, message_func) do
│ ~

└─ lib/ash_phoenix/inertia/error.ex:97:9

Generated ash_phoenix app
Running ExUnit with seed: 514125, max_cases: 24
And a couple of tests are failing
ZachDaniel
ZachDaniel3mo ago
Ah, right Change it to %AshPhoenix.Form{} = form So that it only matches that function head when the first argument is a form 😄
Joan Gavelán
Joan GavelánOP3mo ago
GitHub
fix: ensure nested form errors are included by joangavelan · Pull ...
Contributor checklist Leave anything that you believe does not apply unchecked. I accept the AI Policy, or AI was not used in the creation of this PR. Bug fixes include regression tests Chores ...
Joan Gavelán
Joan GavelánOP3mo ago
Yee haha, that wasn't so difficult (when you're told what to do Lol) Hope that helps

Did you find this page helpful?