A step-by-step introduction to DXN (Data eXchange Notation) itself —
the format, independent of any particular implementation. Every
snippet below is plain .dxn text; try each one with
Dextrin.decode/1 (or any other conformant reader) as you go. See the
reference for the complete, terse specification this
tutorial is building intuition for.
1. Scalars
nil
true
false
42
-17
3.14
NaN
Infinity
19.99M
22/7
"hello, world"
?anil/true/false are exactly what they look like. Integers are
arbitrary precision. Floats are IEEE 754 doubles, with NaN/
Infinity/-Infinity as explicit literals (no sign on NaN). A
trailing M marks a decimal — exact fixed-point, the type you want
for money. A bare int/uint is a rational — stored exactly as
written, 22/7 and 44/14 are different values unless you reduce
them yourself. ?a is a char — one Unicode codepoint, distinct from
the one-character string "a".
2. Symbols and keywords
some-symbol
Point
Namespace/Name
:ok
:error
name:A symbol is a bare, unevaluated identifier — a reference, not a
value in the way a string is. A keyword starts with : in value
position (:ok) or is written as name: immediately before a value,
in key position (name: 1 inside a map). Identifiers may contain a
single / to namespace them (Namespace/Name) — used for
cross-file schema references.
3. Collections
[1 2 3]
{1 2 3}
%{ x: 1, y: 2 }
@ordered %{ first: 1, second: 2 }
@{:a :b :c}
@sorted-set @{3 1 2}[...] is a list. {...} is a tuple — also ordered and
heterogeneous, but conceptually a fixed record rather than a growable
list (an implementation may still store both similarly; the
distinction is about intent). %{...} is a map — note commas are
fully optional everywhere in DXN, so %{x: 1, y: 2} and %{x: 1 y: 2} are identical. Plain maps make no ordering guarantee, even
though entries sit sequentially in the source text — if order matters,
say so explicitly with @ordered %{...}, which makes order part of
the value's identity. @{...} is a set; @sorted-set @{...} is a
set that's always kept sorted.
4. Structs — with and without a schema
%Point{x: 1, y: 2}
%Point[1, 2]Both are the same struct, written two ways: keyed (field names
inline, order doesn't matter) and positional (order matters, no
names). A reader that doesn't have a schema for Point still parses
this successfully — it just can't fully interpret it, and returns an
opaque tagged value instead of failing. A reader that does have a
compiled schema can convert between the keyed and positional forms
freely, validate required/optional fields, and reject data that
doesn't conform. See the reference
(or dextrin's own schema tutorial in its main
tutorial) for how schemas are themselves written —
in DXN, not a separate DSL.
5. Temporal types
~D[2024-01-01]
~T[12:30:00]
~U[2024-01-01 12:30:00Z]
@datetime "2024-01-01T12:30:00+02:00"
@duration "P1Y2M10D"~D[...]/~T[...] are ISO 8601 dates/times. ~U[...] is a UTC
instant. @datetime is for anything with a non-zero offset — the
sigil form is reserved for UTC specifically so a reader can tell the
two apart by syntax alone. @duration uses ISO 8601 duration syntax;
note a duration tracks years/months/weeks/days/hours/minutes/seconds
as independent fields, since years and months are calendar-relative
(a "month" isn't a fixed number of seconds) and can't be folded into
one elapsed-time number.
6. Extended types
@uuid "550e8400-e29b-41d4-a716-446655440000"
@uri "https://example.com/path"
@bytes "SGVsbG8="
~r/^[a-z0-9_]{3,20}$/i@uuid takes the canonical 36-character hyphenated form. @uri is
any RFC 3986 URI, kept as the exact string given. @bytes is raw
binary data, base64-encoded in text. ~r/pattern/flags is a regular
expression — the same PCRE-ish syntax and flag letters (i m s u x f r) most host languages already use for their own regex literals.
7. Custom tags — the open extension point
@my-app/money 500Any @name not matching one of the built-ins above (@uuid,
@duration, @datetime, @bytes, @array, @ordered,
@sorted-set) is a custom tag: a single opaque value wrapped with
an application-defined name. A reader with no decoder registered for
my-app/money still parses this fine — it decodes to an opaque
tagged value, the same graceful-degradation contract structs get.
Reserve custom tags for simple scalar-wrapping; prefer a schema-backed
struct for anything with real field structure.
8. Discarding a value
@_ "this text is parsed but produces nothing"
[1 @_ "skip me" 2 3]@_ parses the value that follows it (so a malformed discarded value
is still a real parse error, not silently swallowed) and then discards
it entirely — it never appears in the resulting value tree, and has
no binary encoding at all.
9. Comments and the optional header
# this is a comment, running to end of line
@dxn "1.0"
%{ x: 1 }# starts a comment that runs to the end of the line; comments are
fully insignificant, exactly like whitespace, and are gone by the time
a value exists — no reader can recover them after parsing. @dxn "1.0" is an optional header, allowed only as the very first thing in
a document, naming the format version.
10. A complete, functional example
Putting all of the above together — a single, realistic document using nearly every type at once:
@dxn "1.0"
%{
id: @uuid "550e8400-e29b-41d4-a716-446655440000"
name: "Ada Lovelace"
active: true
score: 19.99M
tags: @{:admin :staff}
meta: @ordered %{created: ~U[1990-01-01 00:00:00Z]}
address: %Point[51.05, 13.74]
result: {:ok, 200}
handle: ~r/^[a-z0-9_]{3,20}$/i
}Every field here round-trips losslessly through .dxnb, the binary
form — see the reference §2 for the full binary type
mapping, and examples/cheatsheet
for more. If you're working in Elixir, dextrin's own
tutorial picks up exactly here and shows what each
of these types looks like once decoded.