Skip to content

fix: Skip embed form standalone validation#474

Merged
zachdaniel merged 3 commits into
mainfrom
fix/skip-embed-form-standalone-validation
May 14, 2026
Merged

fix: Skip embed form standalone validation#474
zachdaniel merged 3 commits into
mainfrom
fix/skip-embed-form-standalone-validation

Conversation

@sevenseacat

Copy link
Copy Markdown
Contributor

As discussed on Discord, there's a bit of a gap in how AshPhoenix treats forms for embedded resources vs. related resources.

Embedded sub-forms are currently validated standalone against the embed's own :create/:update action, in addition to the parent action's inline cast of the same input. The standalone pass is redundant for embeds, and blocks legitimate patterns such as collapsing entirely-blank input to nil (instead of storing an embed of empty data).

How this was observed

  • cast_input overrides are silently ignored.
    I think it's relatively common to have "all-or-nothing" embedded values (postal address, anything with "fill completely or leave empty" UX), via overriding cast_input/2 on the embed to collapse blank input to {:ok, nil}. The parent's allow_nil?: true attribute then accepts the value as nil.
    Pre-patch, the sub-form's strict standalone validation overrides this choice and the form is invalid for blank input.

  • Sub-form errors duplicate parent-cast errors.
    After carry_over_errors/2 injects parent errors into the sub-form, both sets coexist in source.errors. Downstream renderers see two visible errors per missing field, each tagged with different breadcrumbs.

Why unions are excluded

Unions rewrite their params into an %Ash.Union{} struct before handing the value to the parent, so the parent can't catch input errors at the cast layer — the standalone validation is the only line of defense for unions. The patch tags union sub-forms with union?: true in AshPhoenix.Form.Auto.unions/3 and excludes them.

manage_relationship sub-forms don't have embed?: true set, so the patch's clause doesn't fire for them.

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
  • Documentation changes
  • Features include unit/acceptance tests
  • Refactoring
  • Update dependencies

sevenseacat and others added 3 commits May 14, 2026 16:00
Nested forms backed by an embedded attribute/argument were validated
twice: once against the embed's own create/update action, and again
inline when the parent action cast the merged params. The standalone
pass bypasses parent-level logic (e.g. a `cast_input/2` override on
the embed that collapses blank input to `nil`), so an all-blank embed
sub-form produced spurious "is required" errors even when the parent
would have accepted the value as nil.

After validating, clear `source.errors` and mark the source valid for
forms with `embed?: true`. `carry_over_errors/2` replays any real
errors the parent's validation surfaces with a matching path. Forms
with `transform_params` set are skipped — unions rewrite params into
an `%Ash.Union{}` before handing them up, so their standalone
validation is the only line of defense.
`clear_embed_source_errors/2` used `!opts[:transform_params]` to detect
union sub-forms. `transform_params` is also a public form option, so a
caller setting it on a non-union embed (undocumented but reachable)
would silently disable the exemption. Set `union?: true` in `unions/3`
and check that instead.
Split the embed-defers-validation test into two:

* A structural test asserting that the sub-form's `source.valid?` is
  `true` and field errors flow only through `carry_over_errors/2` from
  the parent. This demonstrates the patch's direct effect without
  relying on any consumer-side `cast_input` override.

* A user-facing test demonstrating what the patch enables: an
  "all-or-nothing" embedded value (whose `cast_input` collapses blank
  input to `nil`) accepting blank input on a parent with a nullable
  embed attribute.

Add a moduledoc to the `AshPhoenix.Test.Address` fixture explaining
the `cast_input` override as the canonical "all-or-nothing embedded
value" pattern rather than test-specific scaffolding.
@sevenseacat sevenseacat changed the title Fix/skip embed form standalone validation fix: Skip embed form standalone validation May 14, 2026
@zachdaniel zachdaniel merged commit f484542 into main May 14, 2026
24 of 25 checks passed
@zachdaniel

Copy link
Copy Markdown
Contributor

🚀 Thank you for your contribution! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants