A conditional_field lets a single name resolve to one of several shapes
depending on the input value. Children share the parent's name; the runtime
walks them in declaration order and the first child whose validator: returns
{:ok, ...} wins. Nesting and :structs lists are both supported.
conditional_field :actor, any() do
field :actor, struct(), struct: Actor, validator: {VAL, :is_map_data}
conditional_field :actor, any(), structs: true, validator: {VAL, :is_list_data} do
field :actor, struct(), struct: Actor, validator: {VAL, :is_map_data}
field :actor, String.t(), validator: {VAL, :is_string_data},
derives: "validate(url)"
end
field :actor, String.t(), validator: {VAL, :is_string_data},
derives: "validate(url)"
endChild-validator contract
Each child must declare validator: {Mod, :fn}. The MFA is called as
Mod.fn(field_name, value) and returns one of:
| Return | Meaning |
|---|---|
{:ok, name, value} | This child wins. Use the (possibly coerced) value. |
{:error, name, reason} | This child loses. Try the next. |
Descent semantics
A conditional_field does not drill into the value — the same value is
fed to each candidate. To descend through a list, use structs: true on the
inner conditional with is_list_data. To drill into a sub-map, use a child
struct: reference whose validator: filters maps.
priority: true
At most one child may be marked priority: true. If that child matches, the
runtime stops and ignores siblings.
Aggregated error shape
When no child matches, the parent emits one error map of action :conditionals:
[
%{
field: :actor,
action: :conditionals,
errors: [
%{field: :actor, action: :validator, __hint__: "actor-map", message: "It is not map"},
%{field: :actor, action: :validator, __hint__: "actor-list", message: "It is not list"},
%{field: :actor, action: :validator, __hint__: "actor-url", message: "It is not string"}
]
}
]Inner conditionals nest the same shape recursively. Use hint: "label" on each
child to disambiguate which arm produced which inner error.
Arbitrary depth
Nested conditional_field works to any depth (closes #7, #8, #25). The runtime
recurses through the same dispatcher; each level adds one layer of :conditionals
aggregation to the error tree.
Common gotchas
- If every nested conditional gates entry with
is_map_databut the deepest child needs an integer, no input can ever reach it — the same value flows through each level. Usestructs: trueto iterate a list, or remove the gate, or restructure with sub_field. - Mixed atom and string keys on the input are normalized by the runtime; validators see atom keys.