ADR-0004 decision 7: whether a value conforms to a type expression.
conforms/3 is given the declarations, a type expression and a value -
a payload a host decoded from JSON or built in Elixir - and answers :ok,
or {:error, mismatches} naming every place the value does not conform.
It checks against a declared record, a declared shape or an inline shape
alike: a payload is checked against what its event declares it carries.
StatifierDatamodel.Types.satisfies/3 decides a type against a type, and
StatifierDatamodel.Coverage.missing/3 decides which required fields of a
shape a map leaves unfilled. This module is the third question beside
them, a value against a type, and neither of the other two changes any
answer it gives.
A mismatch: a path and a reason
Each mismatch is {path, reason}. path is a list of segments, a string
for a field name and an integer for a list index; the value itself is at
[]. reason is one of two:
:missing- a required field is absent ornil, which isStatifierDatamodel.Coverage.missing/3's fill rule;{:expected, type}- the value atpathdoes not conform totype, the type expression it failed.
Mismatches come in the type's field order, depth first, and a list's items in index order.
What each type accepts
| Type | A value conforms when it is |
|---|---|
:string | a binary |
:integer | an integer |
:decimal | an integer, a float, a Decimal struct, or a string |
:boolean | a boolean |
:date | an ISO 8601 date string, or a Date |
:datetime | an ISO 8601 date-time string carrying an offset, or a DateTime |
:duration | an ISO 8601 duration string, an expression-language duration string, or a Duration |
:object | a map |
:list | a proper list, whose items conform to the field's item_type when it is known |
| a declared record or shape, or an inline shape | a map whose fields conform, below |
:unknown, {:opaque, _}, a declared name the declarations do not hold, or nil | anything |
A Decimal struct is recognised by its module name; the package takes no
dependency on it. The string spellings of :decimal and :duration are
ADR-0001 decision 4's, accepted beside the ones a decoded JSON value
spells. Decision 4 says a decimal "is a string" and states no grammar
for it, so any string conforms. An expression-language duration string
is one or more <digits>(.<digits>)?<unit> pairs, the unit one of y,
mo, w, d, h, m, s and ms, with no whitespace and no sign,
and a fraction that comes to a whole number of milliseconds (30s,
1h30m, 1.5s): the rule the expression language's own duration parse
applies to a whole string, followed here without depending on it.
A NaiveDateTime carries no offset and does not conform to :datetime,
and neither does a date-time string without one. A field whose type, or a
list field whose item_type, named nothing the document declares is
read as unknown, as an undeclared name is everywhere in this package.
A record, a shape or an inline shape
The value must be a map, read with the strings the declaration spells, as
StatifierDatamodel.Coverage.missing/3 reads one: a key spelled as an atom
is not the field's key. Then each field, in the type's order:
- absent or
nil- a:missingmismatch when the field is required, and nothing when it is optional; - present - checked against the field's type, and a
listfield's items against itsitem_type, with the field's name, and an item's index, added to the path.
A key the type does not name is ignored, because every object is open, and
a field's one_of is a hint and is not checked. A record and a shape are
checked the same way: the difference between them is what the read check
decides, not what a value carries.
Every answer is a fact. Nothing raises, nothing is enforced at run time, and whether a mismatch stops a host's pipeline is the host's to decide (ADR-0001 decision 12).
Summary
Types
One place a value does not conform to its type.
Where a mismatch is: a field name or a list index per segment, [] the value itself.
Why the value at a path does not conform.
Functions
Whether value conforms to type: :ok, or every mismatch in order.
Types
One place a value does not conform to its type.
@type path() :: [String.t() | non_neg_integer()]
Where a mismatch is: a field name or a list index per segment, [] the value itself.
@type reason() :: :missing | {:expected, StatifierDatamodel.Types.t()}
Why the value at a path does not conform.
Functions
@spec conforms( StatifierDatamodel.Declarations.t(), StatifierDatamodel.Types.t(), term() ) :: :ok | {:error, [mismatch(), ...]}
Whether value conforms to type: :ok, or every mismatch in order.
The library loan. A renewal is a record whose holds are a list of another record:
iex> alias StatifierDatamodel.{Conformance, Declarations}
iex> declarations = Declarations.from_document(%{"types" => [
...> %{"name" => "loans.renewal", "kind" => "record", "label" => "Renewal", "fields" => [
...> %{"name" => "copy_id", "type" => "string", "required?" => true},
...> %{"name" => "due_on", "type" => "date", "required?" => true},
...> %{"name" => "renewals", "type" => "integer", "required?" => true},
...> %{"name" => "holds", "type" => "list", "item_type" => "loans.hold"}]},
...> %{"name" => "loans.hold", "kind" => "record", "label" => "Hold", "fields" => [
...> %{"name" => "patron_id", "type" => "string", "required?" => true}]}]})
iex> Conformance.conforms(declarations, {:declared, "loans.renewal"},
...> %{"copy_id" => "c-0412", "due_on" => "2026-11-02", "renewals" => 1})
:ok
iex> Conformance.conforms(declarations, {:declared, "loans.renewal"},
...> %{"copy_id" => "c-0412", "due_on" => ~D[2026-11-02], "renewals" => 1})
:ok
iex> Conformance.conforms(declarations, {:declared, "loans.renewal"},
...> %{"copy_id" => "c-0412", "due_on" => "next week", "renewals" => nil,
...> "holds" => [%{"patron_id" => "p-77"}, %{}], "shelf" => "B3"})
{:error, [{["due_on"], {:expected, :date}},
{["renewals"], :missing},
{["holds", 1, "patron_id"], :missing}]}An inline shape is checked the same way, and a scalar on its own is the
value at []:
iex> alias StatifierDatamodel.Conformance
iex> Conformance.conforms(%{}, {:shape, [%{name: "patron_id", type: :string, required?: true}]},
...> %{"patron_id" => 77})
{:error, [{["patron_id"], {:expected, :string}}]}
iex> Conformance.conforms(%{}, :duration, "1h30m")
:ok
iex> Conformance.conforms(%{}, :datetime, ~N[2026-11-02 10:00:00])
{:error, [{[], {:expected, :datetime}}]}
iex> Conformance.conforms(%{}, :unknown, {:anything, "at all"})
:ok