GuardedStruct DSL
Reference: usage-rules/dsl.md in the project root.
Quick template
defmodule MyApp.User do
use GuardedStruct
guardedstruct enforce: true, json: true do
field :id, :string, auto: {Ecto.UUID, :generate}
field :email, :string,
derives: "sanitize(trim, downcase) validate(string, not_empty, email_r)"
sub_field :profile, :map do
field :bio, :string, derives: "validate(string, max_len=200)"
field :country, :string, derives: "validate(string, min_len=2, max_len=2)"
end
conditional_field :owner, any() do
field :owner, struct(), struct: MyApp.Person, validator: {Checks, :is_map_data}
field :owner, String.t(), validator: {Checks, :is_string_data},
derives: "validate(url)"
end
virtual_field :password_confirmation, :string
dynamic_field :metadata
end
end
Entity reference
| Entity |
Generates |
Notes |
field |
struct slot |
:dynamic_field kind when dynamic_field macro used. |
sub_field |
nested submodule (MyApp.User.Profile etc.) |
Recursive; own error:/authorized_fields:/main_validator:. |
conditional_field |
one runtime-selected child |
Children share parent's name. ≤ 1 priority: true child. |
virtual_field |
input-only, dropped from struct |
Validated by main_validator; ideal for password_confirmation. No struct/structs/priority. |
dynamic_field |
passthrough map, no key-atomization |
Default validate(map). Atom-attack safe. No struct/structs/priority. |
Section options
guardedstruct enforce: true, # cascade to every child without default
opaque: true, # @opaque t() instead of @type t()
module: MyOther, # emit struct into nested module
error: true, # generate MyApp.User.Error exception
authorized_fields: true, # reject unknown top-level keys
main_validator: {Checks, :consistency},
json: true, # auto-derive Jason/JSON encoder
auto_wire: true # (Ash only) inject Change automatically
Generated functions on every guarded module
defstruct, @enforce_keys, @type t() / @opaque t()
keys/0,1, enforce_keys/0,1 — :all arg recurses sub_fields
example/0 — populated from defaults + type-based placeholders
__information__/0 — full DSL metadata; includes :keys,
:enforce_keys, :conditional_keys, :virtual_keys, :dynamic_keys,
:options
__fields__/0 — full per-field metadata list
__field_meta__/1 — O(1) lookup by name
__guarded_information__/0, __guarded_fields__/0, __guarded_field_meta__/1
— Ash-compatible aliases (always defined, even in standalone)
__guarded_has_validator__/0, __guarded_has_main_validator__/0,
__guarded_error_module__/0, __guarded_derive_extensions_opt__/0
— compile-time-baked predicates / pointers
builder/1,2 — public entry point
@derives decorator (alternative to inline derives:)
A module attribute consumed by the next entity. One-shot, cleared after.
@derives "sanitize(trim, downcase) validate(email_r)"
field :email, :string, enforce: true
Also accepted: @derive_rules. Same semantics.
Don'ts
- Don't call
function_exported? on a guarded module to check for these
generated functions — they're guaranteed at compile time.
- Don't mix atom-keyed and regex-keyed
field declarations in the same
guardedstruct block — classify_shape/1 rejects this at compile time.
- Don't pass
:fields (plural) to enforce — section-level enforce: true
cascades automatically.
1---2name: guarded-struct-dsl3description: Use when writing or modifying a `guardedstruct do ... end` block. Triggers on `field`, `sub_field`, `conditional_field`, `virtual_field`, `dynamic_field` declarations, section options like `enforce:`, `authorized_fields:`, `error:`, `json:`, `main_validator:`, or when the generated module surface (`__information__/0`, `__fields__/0`, `__field_meta__/1`, `keys/0,1`, `enforce_keys/0,1`, `example/0`) is being read or queried.4---56# GuardedStruct DSL78Reference: `usage-rules/dsl.md` in the project root.910## Quick template1112```elixir13defmodule MyApp.User do14 use GuardedStruct1516 guardedstruct enforce: true, json: true do17 field :id, :string, auto: {Ecto.UUID, :generate}18 field :email, :string,19 derives: "sanitize(trim, downcase) validate(string, not_empty, email_r)"2021 sub_field :profile, :map do22 field :bio, :string, derives: "validate(string, max_len=200)"23 field :country, :string, derives: "validate(string, min_len=2, max_len=2)"24 end2526 conditional_field :owner, any() do27 field :owner, struct(), struct: MyApp.Person, validator: {Checks, :is_map_data}28 field :owner, String.t(), validator: {Checks, :is_string_data},29 derives: "validate(url)"30 end3132 virtual_field :password_confirmation, :string33 dynamic_field :metadata34 end35end36```3738## Entity reference3940| Entity | Generates | Notes |41|---|---|---|42| `field` | struct slot | `:dynamic_field` kind when `dynamic_field` macro used. |43| `sub_field` | nested submodule (`MyApp.User.Profile` etc.) | Recursive; own `error:`/`authorized_fields:`/`main_validator:`. |44| `conditional_field` | one runtime-selected child | Children share parent's name. ≤ 1 `priority: true` child. |45| `virtual_field` | input-only, dropped from struct | Validated by main_validator; ideal for `password_confirmation`. No `struct`/`structs`/`priority`. |46| `dynamic_field` | passthrough map, no key-atomization | Default `validate(map)`. Atom-attack safe. No `struct`/`structs`/`priority`. |4748## Section options4950```elixir51guardedstruct enforce: true, # cascade to every child without default52 opaque: true, # @opaque t() instead of @type t()53 module: MyOther, # emit struct into nested module54 error: true, # generate MyApp.User.Error exception55 authorized_fields: true, # reject unknown top-level keys56 main_validator: {Checks, :consistency},57 json: true, # auto-derive Jason/JSON encoder58 auto_wire: true # (Ash only) inject Change automatically59```6061## Generated functions on every guarded module6263- `defstruct`, `@enforce_keys`, `@type t()` / `@opaque t()`64- `keys/0,1`, `enforce_keys/0,1` — `:all` arg recurses sub_fields65- `example/0` — populated from defaults + type-based placeholders66- `__information__/0` — full DSL metadata; includes `:keys`,67 `:enforce_keys`, `:conditional_keys`, `:virtual_keys`, `:dynamic_keys`,68 `:options`69- `__fields__/0` — full per-field metadata list70- `__field_meta__/1` — O(1) lookup by name71- `__guarded_information__/0`, `__guarded_fields__/0`, `__guarded_field_meta__/1`72 — Ash-compatible aliases (always defined, even in standalone)73- `__guarded_has_validator__/0`, `__guarded_has_main_validator__/0`,74 `__guarded_error_module__/0`, `__guarded_derive_extensions_opt__/0`75 — compile-time-baked predicates / pointers76- `builder/1,2` — public entry point7778## `@derives` decorator (alternative to inline `derives:`)7980A module attribute consumed by the **next** entity. One-shot, cleared after.8182```elixir83@derives "sanitize(trim, downcase) validate(email_r)"84field :email, :string, enforce: true85```8687Also accepted: `@derive_rules`. Same semantics.8889## Don'ts9091* Don't call `function_exported?` on a guarded module to check for these92 generated functions — they're guaranteed at compile time.93* Don't mix atom-keyed and regex-keyed `field` declarations in the same94 `guardedstruct` block — `classify_shape/1` rejects this at compile time.95* Don't pass `:fields` (plural) to `enforce` — section-level `enforce: true`96 cascades automatically.