joetjen.net
EN DE
Data format

Values

DXN has 28 data types in four groups: scalars, collections, temporal values and extended types. Each one has exactly one text spelling and one binary encoding.

Format 1.0

At a glance

TypeText
nil, booleannil true false
integer, float-17 3.14 5e-3 NaN -Infinity
decimal, rational19.99M 22/7
string, char"text\n" ?a ?s
symbol, keywordPoint ns/name :ok name:
list, tuple, array[1 2] {:ok 200} @array[1 2]
map, ordered map%{a: 1} %{"k" => 1} @ordered %{…}
set, sorted set@{1 2} @sorted-set @{3 1 2}
struct%Point{x: 1 y: 2} %Point[1 2]
date, time, timestamp~D[2024-01-01] ~T[12:30:00] ~U[2024-01-01 12:30:00Z]
datetime, duration@datetime "2024-01-01T12:30:00+02:00" @duration "P1Y2M10D"
uuid, uri, bytes, regex@uuid "…" @uri "…" @bytes "SGVsbG8=" ~r/^a+$/i
custom tag@my-app/money 500

Numbers

Four kinds, each exact about what it is:

  • Integer — an optional - and digits, with arbitrary precision. There is no + sign, no hex and no digit separator.
  • Float — an IEEE 754 double, with a fraction, an exponent or both. NaN, Infinity and -Infinity are literals; NaN has no sign.
  • Decimal — digits with a trailing M, exact fixed-point. 19.99M stays 19.99, in text and in binary.
  • Rational — numerator/denominator, stored as written: 22/7 and 44/14 are different values. A zero denominator is an error.
[42 -17 12345678901234567890123 3.14 1e3 NaN -Infinity 19.99M -1/3]

Strings and characters

Strings are UTF-8 in double quotes, and may span lines. The escapes are \", \\, \n, \t, \r, \0, \a, \b, \f, \v, and \x{…} with any number of hex digits for a code point. There is no single-quoted form and no interpolation.

A character is ? and one code point — a value of its own, not a one-letter string. ?s is the space, and escapes work after ? too.

["tab\there, smile \x{1F600}" ?a ?s ?\n ?é]

Symbols and keywords

A bare identifier is a symbol: a name that refers to something — a type, a handler, a schema — and is never evaluated. A keyword is a name used as a value, like a status or a mode: :ok, or :"with spaces" when it is not a plain identifier. In a map, name: before a value is a keyword key.

%{
  handler: billing/invoice-created   # a symbol
  status:  :ok                       # a keyword
  label:   :"needs review"
}

Collections

FormTypeMeaning
[…]listan ordered sequence that may grow
{…}tuplea fixed group — a pair, a result, a record
@array[…]arraya fixed-size, indexed sequence
%{…}mapkeys to values, in no particular order
@ordered %{…}ordered mapa map whose order is part of its identity
@{…}setdistinct values, unordered
@sorted-set @{…}sorted seta set kept in sorted order

A map entry is either name: value, for keyword keys, or key => value, for keys of any type. The two styles mix freely. A plain map promises no order even though its entries are written one after another; if order matters, say so with @ordered.

%{
  name: "api"
  "Content-Type" => "application/json"
  404 => :not-found
  {52.52 13.40} => "Berlin"
}
@sorted-set @{3 1 2}
@sorted-set @{1 2 3}

Structs

A struct is a named record, written keyed — field names inline, in any order — or positional, with the values in the order the schema defines. Both forms are the same value.

%Point{y: 2, x: 1}
%Point[1, 2]

A struct parses without a schema: a reader that does not know Point returns an opaque value carrying the name and the fields as written. With a schema, it can check the fields, convert between the two forms and give the fields their types.

Dates and times

FormTypeMeaning
~D[2024-01-01]datea calendar date
~T[12:30:00.5]timea time of day, up to microseconds
~U[2024-01-01 12:30:00Z]timestampan instant in UTC — always with Z
@datetime "2024-01-01T12:30:00+02:00"datetimean instant with a non-zero UTC offset
@duration "P1Y2M10DT2H30M"durationan ISO 8601 duration

UTC and offset instants are told apart by syntax alone: the sigil is for UTC, the tag for everything else. A duration keeps years, months, weeks, days, hours, minutes and seconds as separate fields, because a month or a year has no fixed length and cannot be folded into one number of seconds.

UUIDs, URIs, bytes and regexes

  • @uuid "…" takes the 36-character hyphenated form; anything else is an error.
  • @uri "…" is an RFC 3986 URI, kept exactly as written.
  • @bytes "…" is raw binary data, base64 in text and plain bytes in .dxnb.
  • ~r/pattern/flags is a regular expression, with the flags i m s u x f r — caseless, multiline, dotall, unicode, extended, firstline, ungreedy.
%{
  id:     @uuid "9b74c989-1e2a-4b17-9b1f-2a6a4a2b6f10"
  home:   @uri "https://example.com/path"
  avatar: @bytes "SGVsbG8="
  handle: ~r/^[a-z0-9_]{3,20}$/i
}

Custom tags

Any tag name that is not built in is a custom tag: one value wrapped in an application-defined name. A reader with a decoder for the name turns it into its own type; a reader without one keeps it as an opaque tagged value and passes it on. Namespace the name to keep it from colliding with someone else’s.

@my-app/money {1999 :eur}

Custom tags suit a single wrapped value. Anything with real fields is better written as a struct, which a schema can check.