Native schemas: Formata.Backplane.Schema definitions used through Formata.
Creating schemas
A native schema is plain Elixir data in the Formata.Backplane.Schema vocabulary:
definition = %{
name: {:required, {:string, min: 1}},
age: {:integer, gte: 0},
role: {:enum, ["admin", "member"]},
tags: {:list, :string},
address: %{city: {:required, :string}}
}
{:ok, schema} = Formata.Schema.native(definition)Atom schema keys match atom or string data keys, so the definition above validates string-keyed documents decoded by Formata.
Reusable definitions
Because definitions are data, they can be bound to variables and composed
(merge/2, select/2, except/2 delegate to Formata.Backplane.Schema). Named,
reusable and recursive definitions use Formata.Backplane.Schema.defschema/3 and
{:ref, {Module, name}}:
defmodule MyApp.Schemas do
import Formata.Backplane.Schema
defschema :address, %{city: {:required, :string}}
defschema :person, %{
name: {:required, :string},
address: {:ref, :address},
friends: {:list, {:ref, :person}}
}
end
{:ok, schema} = Formata.Schema.native({:ref, {MyApp.Schemas, :person}})Pure validation (Formata.validate/3)
Formata.Backplane.Schema.validate/3 both validates and transforms (defaults,
coercion, transforms, and strict-mode stripping/renaming of keys). Formata's
pure validation therefore runs a preflight over the definition, including
every reachable {:ref, ...} target, and supports this non-transforming
subset:
- primitive types,
{:literal, v},{:enum, ...},{:required, ...},{:meta, ...} - string/integer/float constraints (
min,max,regex,eq,neq,gt,gte,lt,lte,range,multiple_of) - nested maps, keyword schemas,
{:list, ...},{:map, ...},{:tuple, ...},{:schema, ...}(including:additional_keys) {:either, ...},{:oneof, ...}(first matching branch, native semantics),{:multi, ...},{:cond, ...},{:dependent, field, check, type}{:custom, ...}validators that return:okor{:error, ...}{:ref, {Module, name}}, including recursive references
These constructs are rejected before anything runs, with an :unsupported /
:pure_validation_unsupported error naming the construct and its schema
path: {type, {:default, _}}, {type, {:transform, _}},
{type, {:encode, _}}, every {:coerce, ...} form, and the dynamic
{:dependent, callback} forms (their schemas are only known at run time).
Use Formata.cast/3 for those.
Custom validators run inside the native engine. If one returns anything
other than :ok or an error during pure validation (for example {:ok, new_value}),
validation fails with :unsupported / :non_pure_custom instead of
accepting a modified value.
The input is never modified, and validate returns only :ok or errors.
Absent versus nil
The native engine treats an explicit nil like an absent field: an
optional field accepts both, and {:required, type} rejects both.
Unknown fields
| Option | validate/3 | cast/3 |
|---|---|---|
unknown: :allow | default; ignored | kept in the result |
unknown: :reject | :unknown_field error per field | :unknown_field error per field |
unknown: :strip | not accepted | default; removed from the result |
Unknown-field rejection is enforced by the native engine (Formata adds an
:additional_keys rule to each map schema). It is not available for keyword
list schemas.
Casting (Formata.cast/3)
cast runs the full native engine (Formata.Backplane.Schema.validate/3) and returns
the transformed data. Modifiers wrap types, and each field is processed from
the outermost modifier inwards:
{type, {:default, value}}replaces an absent ornilvalue withvalue; the default is then processed bytypelike any input;{:coerce, ...}converts the source representation (for example"42"to42) and passes values that already have the target type;- the type, its constraints,
{:custom, ...}validators and nested schemas are checked on the (possibly defaulted/coerced) value; {type, {:transform, fun}}maps the validated value. Transforms only run for present, non-nilvalues, so write{{type, {:transform, fun}}, {:default, value}}to transform a default.
A default can never satisfy {:required, ...}: Formata.Backplane.Schema rejects
that combination when the schema is compiled.
Unknown fields are then handled by the :unknown option. With :strip (the
default) the result contains only declared fields, keyed by the schema's keys
(an atom-keyed schema returns atom keys). With :allow, the result keeps the
input keys: a declared field is written back under the key present in the
data (an atom schema key matching a string data key keeps the string key),
undeclared fields are kept unchanged, and a field added by a default uses the
schema's key.
Coercion to :atom! is rejected (:unsafe_atom_coercion) because it would
create atoms from input data.
Summary
Functions
Checks and compiles a native definition.
Functions
@spec compile(term(), keyword()) :: {:ok, Formata.Schema.t()} | {:error, [Formata.Error.t(), ...]}
Checks and compiles a native definition.
Invalid definitions return :schema / :invalid_schema errors. A definition
that is valid but outside the pure-validation subset still compiles; the
limitation is reported when Formata.validate/3 is called.