Start from the data
Nobody wants to write a schema before breakfast. On schemas inferred from the data you already have, tightened by hand, compiled to a contract a certified runtime can scan — and on hub extension, the move that turns your private schema into a citizen of the graph.
Schema languages have a cold-start problem. The value of a schema arrives later — at the boundary, in the incident that didn’t happen — but the cost lands now, in a syntax you use twice a year, describing shapes your data already knows. So schemas get skipped, and the boundary stays soft.
kaiv’s answer is to let the data go first.
Infer, then tighten
Any example document — kaiv or anything the importers read — yields a working schema:
$ cat example.json
{"name":"eu-1","port":8443,"tags":["prod","eu"]}
$ kaiv infer --name acme/node example.json
.!saiv 1 acme/node
name=
!int
port=
/@tags;=
That is a valid, immediately usable schema: name is a
string, port an integer, tags a vector. It is also — by
design — the weakest true statement about your example, and
authored .saiv is plain kaiv, so tightening it is ordinary
editing: bound the port, pin the tag alphabet, mark what may
be absent. The workflow is not “design a schema, then hope
the data complies”; it is “let the data confess its shape,
then negotiate.”
The contract is compiled
An authored schema is for people. What validators consume is its compiled form — and the compilation is where the type-system machinery disappears into flat constraint lines:
$ cat server-endpoint.saiv
.!saiv 1 hub/server-endpoint
!str
host=
!int[0,65535]
port=
!int[1,3600]
timeout_ms?=500
$ kaiv schema server-endpoint.saiv
.!csaiv 1 hub/server-endpoint
!str'::host=
/^-?[0-9]+$/ ..num [0,65535]'::port=
/^-?[0-9]+$/ ..num [1,3600]'::timeout_ms?=500
One field definition per line, in declared order — and order is load-bearing: canonical documents carry fields in schema order, so validation is a parallel scan, two pointers walking two texts in lockstep, constant memory, no lookahead. The same property that lets a pacemaker parse kaiv lets it validate kaiv.
By default a schema is relaxed — undefined fields flow past
it untouched. The strict modifier closes the namespace:
$ printf 'host=a\nextra=b\n' | kaiv build | kaiv validate - tight.saiv
kaiv: UndefinedFieldStrictSchemaError: ::extra is not defined in the strict schema (line 3)
Relaxed schemas describe what must be true; strict schemas also forbid what was never agreed. Both are one keyword apart, and the choice is printed in the contract.
Extending the hub
The ecosystem’s hub microschemas earn their keep here. A schema can extend one — inherit its fields, in its declared order, under a namespace of your choosing:
$ printf '.!saiv 1 acme/gateway\n.!schema:/upstream hub/server-endpoint\n\n!str\nregion=\n' | kaiv schema
.!csaiv 1 acme/gateway
!str'/upstream::host=
/^-?[0-9]+$/ ..num [0,65535]'/upstream::port=
/^-?[0-9]+$/ ..num [1,3600]'/upstream::timeout_ms?=500
!str'::region=
One declaration — .!schema:/upstream hub/server-endpoint —
and the gateway schema speaks the hub’s dialect for its
upstream block, verbatim, plus its own fields beside it. The
flat form (.!schema:hub/x) inherits at root; the
array-element form (.!schema:/@servers hub/x) applies the
hub to every element of a collection. And because extension
is structural identity by declaration, the registry can
derive the mapping edge to the hub automatically — extension
and mappings are two doors into
the same graph: extend when the hub’s names and order suit
you; map when you want your own.
Other people’s schemas convert — honestly
Teams arrive with schemas already written. The importers
convert JSON Schema, Protocol Buffers, Avro, GraphQL, and XSD
into authored .saiv — as a sound weakening: constraints
kaiv can express arrive intact, constraints it cannot are
dropped visibly, never approximated into something the
original never said:
$ cat server.schema.json
{"type":"object","properties":{"host":{"type":"string","format":"hostname"},"port":{"type":"integer","minimum":1,"maximum":65535},"batch":{"type":"integer","multipleOf":5}},"required":["host","port"]}
$ kaiv import-schema --json --name acme/server server.schema.json
.!saiv 1 acme/server
.!types std/net
&hostname
host=
!int[1,65535]
port=
// dropped: multipleOf at root/batch
!null|int
batch?=
The format: hostname became the pinned std/net type; the
bounds came through; multipleOf — which kaiv’s constraint
triple cannot state — became a comment at the exact field it
guarded, so the weakening is reviewable line by line. A
converted schema never claims more than the original and
never silently claims less than it admits to.
The confession, notarized
Follow the arc: the data confessed its shape (infer); you
negotiated the details (plain-text editing); the contract
compiled to something a certified scanner enforces
(.csaiv); the hub extension made your schema a citizen of
the shared graph; and publication — write-once, like every
registry artifact — makes the agreement permanent. At no
point did you write in a schema-only language, and at no
point did the machine-facing form drift from the
human-facing one: they are the same six-rule text, one
compilation apart.
Schemas skipped because they cost too much protect nobody. The whole design of this layer is to make the honest schema the lazy option.