kaiv for YAML Users — A Cookbook

for kaiv 0.4.0

1 Introduction

You write YAML — for services, pipelines, deployments, playbooks. This cookbook maps YAML onto kaiv, a text format with none of YAML’s ambient hazards: no implicit typing, no indentation sensitivity, no anchors that only some parsers resolve. Every kaiv line carries its own type, its own full address, and its own value, and a schema layer travels with the format instead of living in a separate linter.

kaiv covers YAML’s data model completely — YAML is a superset of JSON, kaiv covers JSON at 100%, and the YAML-specific surface (anchors, merge keys, block scalars, tags) maps or rides a typed escape hatch. Conversion is a hub, not a migration: what comes in from YAML can leave as JSON, XML, CBOR, TOML, or the binary schema formats, and YAML remains your authoring surface for as long as you want it.

Every example is a verified transcript of the reference kaiv tool (cargo install kaiv-cli), run against the fixture files shipped beside this document.

2 Hello, kaiv

A service list, the shape you deploy every week:

region: eu-west
replicas: 3
services:
  - name: web
    port: 8080
    public: true
  - name: db
    port: 5432
    public: false

Import, then build to canonical form:

$ kaiv import app.yaml | kaiv build
.!kaiv 1
!str'::region=eu-west
!int'::replicas=3
!str'/@services/0::name=web
!int'/@services/0::port=8080
!bool'/@services/0::public=true
!str'/@services/1::name=db
!int'/@services/1::port=5432
!bool'/@services/1::public=false

One line per field: !type\,'\,address\,=\,value. The sequence became an indexed @-array; nesting became path segments. There is no indentation to preserve — each line is self-contained, movable, and diffable on its own. The only structural characters are ' and =.

3 The Mapping

YAML kaiv
mapping namespace /services...
sequence @-array /@services/0::name=web
scalar types explicit !type !int, !bool, !null
~ / empty !null !null then tilde=
anchors & aliases resolved at import (values inlined)
merge key <<: resolved at import overrides honored
block scalars typed embed channel &json + base64url
tags (!!str) type annotations !str
multi-document stream not covered one document per file

4 The End of Implicit Typing

YAML’s most famous hazard: under YAML 1.1 rules, NO means false — ask Norway. yes, on, 0o77, 1e3, and bare dates each silently become something other than a string in some parser somewhere, and two YAML consumers routinely disagree.

kaiv’s importer reads YAML 1.2 (the strict rules), and what comes out the other side is pinned:

countries:
  - SE
  - NO
  - DK
answers:
  yes_11: yes
  on_11: on
  norway: NO
version: 1.0
octal: 0o77
big: 1e3
date: 2001-01-23
tilde: ~
$ kaiv import norway.yaml
.!kaiv 1

/@countries;=SE;NO;DK
/answers:=yes_11=yes|on_11=on|norway=NO
!float
version=1.0
!int
octal=63
!float
big=1e3
date=2001-01-23
!null
tilde=

NO is the string NO, in the country list and in the mapping. yes and on are strings. The octal literal resolved to 63 once, at import — and is an explicit !int forever after. The date stayed a string; nothing invents a datetime behind your back. After conversion there is no second parse and no second opinion: the type is on the line.

5 Anchors Resolve, Results Stay Flat

Anchors, aliases, and merge keys are YAML’s reuse mechanism — and a classic source of parser disagreement (merge keys are YAML 1.1 only). kaiv resolves them at import, overrides included:

defaults: &base
  retries: 3
  timeout: 30
prod:
  <<: *base
  timeout: 60
staging:
  <<: *base
$ kaiv import anchors.yaml
.!kaiv 1

!int
/defaults:=retries=3|timeout=30
!int
/prod:=retries=3|timeout=60
!int
/staging:=retries=3|timeout=30

prod kept its override (timeout=60); staging inherited the base. The canonical result carries no references at all — nothing left to resolve, ever again. (For authoring reuse, kaiv has its own variables — .name definitions, $.name references, whole-namespace templates — which likewise resolve away at build time.)

6 Multiline Strings Ride a Typed Channel

kaiv values are single-line by design — that is what keeps the grammar regular and the validator certifiable. A YAML block scalar imports onto the typed embed channel:

$ printf 'motd: |\n  line one\n  line two\n' | kaiv import --yaml
.!kaiv 1
.!types std/enc

&json
motd=ImxpbmUgb25lXG5saW5lIHR3b1xuIg

The value is the JSON string encoding of the text, base64url-armored, typed &json from the std/enc library — lossless, declared, and round-trippable, not a mangled paragraph. If you expected prose to stay readable on the line, that is the honest cost of single-line values; where multiline text dominates, YAML remains the better authoring surface.

7 One Document per File

YAML streams several documents through one file with --- separators. kaiv deliberately does not — a document is a file, and boundaries belong to the filesystem or the archive, not the byte stream:

$ printf -- '---\na: 1\n---\nb: 2\n' | kaiv import --yaml
kaiv: expected one YAML document, found 2

Loud refusal, not silent truncation. Split the stream, import each document.

8 The Round Trip

Back out to YAML, structure intact:

$ kaiv import app.yaml | kaiv build > app.daiv
$ kaiv export --yaml app.daiv
---
region: eu-west
replicas: 3
services:
  - name: web
    port: 8080
    public: true
  - name: db
    port: 5432
    public: false

And because the canonical form is format-neutral, the same app.daiv exports to JSON, XML, CBOR, TOML, and the rest — see the sibling cookbooks.

9 A Schema from Your Example

YAML has no schema language of its own — validation lives in external linters, if anywhere. kaiv infers a schema from the document you already have:

$ kaiv infer --name acme/app app.yaml
.!kaivschema 1 acme/app

region=
!int
replicas=
[/@services]
name=
!int
port=
!bool
public=
[]

The [/@services] … [] block declares the array’s element shape: every element must carry name, port, public, in order, with those types. Break the contract and the error names everything:

$ kaiv infer --name acme/app app.yaml > app.saiv
$ sed 's/public: false/public: maybe/' app.yaml > broken.yaml
$ kaiv import broken.yaml | kaiv build > broken.daiv
$ kaiv validate broken.daiv app.saiv
kaiv: ConstraintViolationError: /@services/1::public=maybe (type !str) violates {true,false} (line 9)

Field, value, violated constraint, line. Exit code 1; pass and exit 0 otherwise.

10 The Constraint Is the Type

One more YAML classic: quote a number, and it is a different value in every typed consumer. Watch what kaiv does with a quoted port:

$ sed 's/port: 5432/port: "5432"/' app.yaml > quoted.yaml
$ kaiv import quoted.yaml | kaiv build > quoted.daiv
$ grep 'port' quoted.daiv
!int'/@services/0::port=8080
!str'/@services/1::port=5432
$ kaiv validate quoted.daiv app.saiv
pass

The quoted port imported as a string — and still validates against the schema’s !int field. That is kaiv’s structural typing at work: str is the only primitive, and !int is a refinement of it — a pattern and a numeric ordering. The serialization 5432 satisfies the refinement, so it is an int, whatever annotation the importer stamped. The type system judges the value, not the label — one more ambiguity that stops mattering after the hub.

11 When to Stay with YAML

An honest map marks the roads not taken. YAML remains the right choice when:

kaiv’s pull grows with what surrounds the data: explicit types, schemas inferred and enforced, validation errors that name the field, units, provenance, a canonical form that diffs line-by-line — and conversion back out at any moment, so the door never locks behind you.

12 Where Next

From here: