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.