conditional_field — runtime child dispatch

Copy Markdown View Source

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)"
end

Child-validator contract

Each child must declare validator: {Mod, :fn}. The MFA is called as Mod.fn(field_name, value) and returns one of:

ReturnMeaning
{: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_data but the deepest child needs an integer, no input can ever reach it — the same value flows through each level. Use structs: true to 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.