Schemas
A .dxns schema is an ordinary DXN document: a map from names to struct schemas and named types. It tells a reader what a struct’s fields are called, in which order they come, what they may hold, and which may be left out.
A schema document
%{
Percentage: {:refine :float %{min: 0.0, max: 100.0}}
Tag: {:refine :string %{min-length: 1, max-length: 20}}
Address: %schema{
fields: @ordered %{
street: :string
city: :string
zip?: :string
}
}
Customer: %schema{
closed: true
fields: @ordered %{
name: :string
address: Address
loyalty: Percentage
tags?: {:list-of Tag}
}
}
}Address and Customer are struct schemas. Percentage and Tag are named types — reusable names for a type expression, referred to exactly like a struct name.
Type expressions
| Form | Matches |
|---|---|
:any | any value |
:integer, :string, … | one keyword per type name |
Address, Ns/Name | a struct schema or named type, by name |
{:list-of T}, {:set-of T} | a list or set whose every element matches T |
{:tuple-of A B C} | a tuple of exactly these slots |
{:map-of K V} | a map with keys K and values V |
{:enum :a :b …} | one of the given literals |
{:one-of A B}, {:all-of A B} | at least one, or every, variant |
{:nilable T} | nil or T |
{:refine T %{…}} | T plus constraints |
%schema{…} | a struct shape |
The constraints of refine are min, max, exclusive-min, exclusive-max and multiple-of for numbers; min-length, max-length and pattern for strings, where length counts code points; and min-count and max-count for lists, sets and tuples. A value must match the base type and every constraint.
Struct schemas
fields: must be an ordered map: its order is the one positional structs and the binary form use. A key ending in ? marks an optional field; the ? is not part of the name. A field that needs more than a type uses %field{type: …, default: …, description: …}.
| Key | Default | Effect |
|---|---|---|
closed: | false | reject any field not listed in fields: |
forbidden: | [] | reject these fields by name, even when the schema is open |
refine-fn: | — | name a whole-value check that lives outside the document |
Money: %schema{
closed: true
forbidden: [legacy_amount_cents]
fields: @ordered %{
amount: :decimal
currency: {:enum :usd :eur :gbp}
}
}forbidden: gives a retired field its own error message instead of a generic “unknown field”.
Named types and other files
Every top-level entry that is not a %schema{} is a named type. Whether a named type may refer to another one in the same document is left to the implementation, since a map has no order to resolve it by; referring to a type from a document loaded earlier always works.
Namespace/Name refers to a schema in another file. The format fixes only this syntax; how a reader finds the file — a path, a registry, a download — is its own business.
%{
Customer: %schema{
fields: @ordered %{
name: :string
home: Address/Address
}
}
}Enforcement
Without a schema, a struct is an opaque value, never an error. Once a reader knows the schema, every value of that struct must have its required fields, field values of the declared types, no unlisted field if the schema is closed, and no forbidden field. A violation is an error of its own kind, distinct from a parse error.
%Money{amount: 9.99M, currency: :eur}
%Money[9.99M :eur]
%Money{amount: 9.99M, currency: :chf}
%Money{amount: 9.99M, currency: :eur, legacy_amount_cents: 999}
%Money[9.99M]ok ok — the same value struct "Money" violates its schema: field "currency" does not match its declared type struct "Money" violates its schema: field "legacy_amount_cents" is forbidden struct "Money" violates its schema: expected 2 positional field(s), got 1