Schema Language
The ZAP schema language — whitespace-significant, brace-free, with optional explicit ordinals.
Schema Language
A ZAP schema describes your data structures and service interfaces in a single
.zap file. Its defining feature is that it is whitespace-significant:
blocks are delimited by indentation — the offside rule — so there are no braces
and no semicolons. Field ordinals are assigned automatically, so you don't write
@N by hand either.
This page is the reference for that syntax. Every example shows the ZAP form alongside the equivalent legacy Cap'n Proto form — ZAP's compiler still parses the brace form for backward compatibility, and the two produce byte-identical schemas.
The offside rule
Indentation defines structure. A struct or interface opens a block; the
members are the lines indented under it. Dedenting closes the block. There is no
file ID and no @N ordinal in the common case:
struct Point
x Float32
y Float32A field is name Type. Ordinals (@0, @1, …) are implied by declaration
order — the first field is @0, the next @1, and so on.
Why ordinals matter — and stay implicit
Ordinals are the basis of forward and backward compatibility: a reader locates a field by its ordinal, not its name or position, so renaming a field or adding a new one never breaks an existing peer. Because ordinals follow declaration order, the rule for safe evolution is simple: only ever append fields. Appending a field gives it the next ordinal and leaves every existing field's ordinal unchanged.
If you need to insert or reorder fields in the source while preserving wire
compatibility, pin the ordinal explicitly with @N. This is the one case where
you write an ordinal by hand:
struct Person
name Text
email @3 Text # pinned: keep ordinal 3 even though it appears 2nd
birthdate Date # implicit ordinal 1
phones List(PhoneNumber)Within a block, implicit ordinals fill the gaps left by pinned ones in ascending order. Mixing implicit and pinned ordinals is exactly how you migrate a legacy brace schema field-by-field without a wire break.
Scalar types
| Category | Types |
|---|---|
| Boolean | Bool |
| Signed integers | Int8 Int16 Int32 Int64 |
| Unsigned integers | UInt8 UInt16 UInt32 UInt64 |
| Floating point | Float32 Float64 |
| Bytes / text | Data Text |
| Other | Void Date |
Lists
List(T) is a homogeneous, variable-length sequence. Elements may be scalars,
structs, or nested lists:
struct Matrix
rows List(List(Float64))
struct Roster
members List(Person)
tags List(Text)Enums
An enum is a closed set of named values. As with struct fields, the values'
ordinals follow declaration order:
enum Color
red
green
blueUnions
A union holds exactly one of its variants at a time — a tagged sum type. It is
declared as an indented block inside a struct:
struct Shape
union
circle Circle
rectangle Rectangle
empty VoidInterfaces
An interface declares callable methods. A method is
name (params) -> (results); both parameter and result lists are
comma-separated name Type pairs. Methods, like fields, get implicit ordinals
in declaration order — appending a method never renumbers the existing ones:
interface AddressBook
lookup (id UInt64) -> (person Person)
insert (person Person) -> (id UInt64)
search (query Text) -> stream (person Person)A result list of stream (...) makes the method return a stream of results
rather than a single one.
Comments
# begins a comment that runs to end of line. Comments are valid anywhere
whitespace is — most usefully to record why a field exists, since the field's
position no longer encodes that:
struct Person
name Text
birthdate Date # added in v1
email Text # added in v2 — readers older than v2 simply skip itImports
using import pulls types from another schema file into the current namespace:
using import "common.zap"
struct Order
customer Person # defined in common.zap
total MoneyWhitespace, formally
- Blocks are indentation-delimited. Indent to open a block under
struct,interface,enum, orunion; dedent to close it. Be consistent within a file — pick spaces or tabs and keep the step uniform. - One declaration per line. A field, method, enum value, or union variant is a single line.
- No braces, no semicolons, no file ID. The compiler's
desugarpass lowers the indented form to the core grammar before any other stage runs. - Brace form still parses. Existing
.capnp-style schemas compile unchanged; desugaring is a no-op on them, so a mixed migration is byte-safe.