GuardedStruct DSL — field, sub_field, conditional_field, virtual_field, dynamic_field
Copy Markdown
View Source
Every entity lives inside one guardedstruct do … end block.
field name, type, opts
field :email, :string,
enforce: true,
default: nil,
derives: "sanitize(trim, downcase) validate(string, not_empty, email_r)",
validator: {MyApp.Checks, :no_disposable_email},
auto: {Ecto.UUID, :generate},
from: "headers::auth_user_id",
on: "profile::owner_id",
hint: "primary-email"| Option | Type | Purpose |
|---|---|---|
name (positional) | atom | Field name. |
type (positional) | quoted type | E.g. :string, String.t(), :integer. |
enforce | boolean | Add to @enforce_keys. Missing input → :required_fields error. |
default | quoted | Default if input omits the key. |
derives | string | Op-string (see guarded_struct:derive). |
derive | string | Legacy singular alias for derives. Honored for 0.0.x compat; prefer derives. |
validator | {Mod, :fn} | Per-field validator MFA, called as Mod.fn(name, value). |
auto | {Mod, :fn} or {Mod, :fn, arg} | Auto-fill the field. |
from | string | Pull value from another path. |
on | string | Conditional rule referring to a path. |
domain | string | Cross-field constraint expression. |
struct | atom | Build value via Mod.builder/1 (external GuardedStruct). |
structs | atom or boolean | List-of items via Mod.builder/1, or true for list-of-self. |
hint | string | Label propagated into the field's error maps. |
priority | boolean | Conditional-field short-circuit marker. |
sub_field name, type, opts do … end
Defines a nested submodule (e.g. MyApp.User.Profile) generated at compile time.
Same options as field plus:
error: true— generates a per-levelErrorexception.authorized_fields: true— reject keys not declared in this sub_field.main_validator: {Mod, :fn}— runs after every nested field validates.
Children: nested field, sub_field, conditional_field.
conditional_field name, type, opts do … end
Pick one child based on the input value. Children share the parent's name.
Each child is tried in order; the first whose validator: returns {:ok, ...}
wins. Use priority: true on at most one child to short-circuit.
structs: trueon the conditional — iterate a list, apply children per element.Aggregated error shape (on no match):
%{field: name, action: :conditionals, errors: [child_attempt_errors...]}Each inner
errorsentry follows the canonical error shape.
virtual_field name, type, opts
Same surface as field, but the value is dropped from the final struct.
Useful for password_confirmation-style inputs consumed by main_validator
but not persisted. Schema accepts name, type, enforce, default,
derives (+ legacy derive), validator, auto, from, on, domain,
hint. Does not accept struct, structs, or priority.
dynamic_field name, opts
Free-form map slot. The inner map is identity-preserved — keys are not
atomized at any depth. Used for attacker-controlled metadata where atom-table
growth would be a DoS risk. Schema accepts the same options as virtual_field
(no struct / structs / priority).
Defaults baked into the entity schema:
type→map()default→%{}derives→"validate(map)"
The entity also auto-sets __dynamic__: true (internal flag — drives the
:dynamic_field kind in __fields__/0 and tells Parser.convert_to_atom_map
to leave inner values untouched).
@derives "..." / @derive_rules "..." decorators
Module attributes consumed by the next entity declaration. One-shot — the
attribute is cleared after the consuming entity, like @doc. Useful for
keeping fields short:
@derives "sanitize(trim, downcase) validate(string, not_empty, email_r)"
field :email, :string, enforce: true
@derives "sanitize(trim) validate(string, max_len=24)"
field :nickname, :stringAvailable on field, sub_field, conditional_field, virtual_field,
dynamic_field.
Section options — guardedstruct opts do … end
guardedstruct enforce: true, authorized_fields: true, json: true do
...
end| Option | Default | Purpose |
|---|---|---|
enforce | false | Cascade enforce: true to every child without a default. |
opaque | false | Generate @opaque t() instead of @type t(). |
module | nil | Emit the struct into a sub-module name. |
error | false | Generate a Module.Error exception. |
authorized_fields | false | Reject unknown top-level keys with :authorized_fields error. |
main_validator | nil | {Mod, :fn} runs after per-field validators. |
validate_derive | nil | User-supplied validator module(s) for unknown ops. |
sanitize_derive | nil | User-supplied sanitizer module(s) for unknown ops. |
json | false | Auto-derive Jason.Encoder (or built-in JSON.Encoder ≥ 1.18). |
auto_wire | false | Ash-only: inject GuardedStruct.AshResource.Change automatically. |
Pattern-keyed maps
A field whose name: is a regex makes the module a pattern map — its
builder/1 returns a plain map keyed by matching string keys, no defstruct.
Use for shard tables, dynamic configuration.
What gets generated
Every guarded module gains at compile time:
defstruct,@enforce_keys,@type t()/@opaque t(),keys/0,1,enforce_keys/0,1,example/0,__information__/0,__fields__/0,__field_meta__/1,__guarded_information__/0,__guarded_fields__/0,__guarded_field_meta__/1(Ash-compatible aliases),builder/1,2,- compile-time flags:
__guarded_has_validator__/0,__guarded_has_main_validator__/0,__guarded_error_module__/0,__guarded_derive_extensions_opt__/0, - sub_field submodules with the same surface.