joetjen.net
EN DE
Data format

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.

Format 1.0

A schema document

customer.dxns
%{
  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

FormMatches
:anyany value
:integer, :string, …one keyword per type name
Address, Ns/Namea 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: …}.

KeyDefaultEffect
closed:falsereject 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.

Read with the schemas above loaded
%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