Skip to content
Jennifer Programming Language

yaml - YAML decode / encode

Enable with use yaml;. YAML 1.2 decode / encode onto the same opaque, library-owned value that json and toml use - the same read / walk / write surface, name for name, so a program that reads, walks, and builds JSON or TOML does the same with YAML. Like TOML, YAML has a timestamp scalar JSON lacks, surfaced through yaml.asDatetime (backed by time).

yaml.decode(text) returns an opaque yaml.Value (a KindObject, the sibling of json.Value / toml.Value); [index] and .field reject it (and == / != compare two handles by value), so the accessors below are the way inside. convert.typeOf reports "object"; convert.objectType reports "yaml.Value".

Unlike json / toml / xml, which are hand-rolled, yaml is backed by a Go dependency (gopkg.in/yaml.v3): full YAML - anchors and aliases, flow and block styles, implicit typing, and multi-document streams - is impractical to hand-roll and has no Go standard library. The dependency is verified TinyGo-clean, so yaml builds on both binaries (jennifer and jennifer-tiny).

Surface

CallReturnsNotes
yaml.decode(text)yaml.ValueParse a single-document YAML string into an opaque handle. A multi-document stream errors (use decodeAll); an empty string decodes to null.
yaml.decodeAll(text)list of yaml.ValueParse every document of a ----separated stream.
yaml.encode(v)stringRender to compact flow style ({a: 1, b: [x, y]}).
yaml.encodePretty(v)stringRender to readable block style (indented, one entry per line).
yaml.typeOf(v[, ptr])stringNode type: null / bool / int / float / string / bytes / list / map / datetime.
yaml.get(v[, ptr])yaml.ValueThe addressed sub-node, re-wrapped so a walk stays opaque.
yaml.has(v, ptr)boolWhether the pointer resolves.
yaml.keys(v[, ptr])list of stringKeys of the addressed mapping, in document order.
yaml.length(v[, ptr])intElement count of a list, entry count of a map.
yaml.asInt(v[, ptr])intStrict: a float node errors.
yaml.asFloat(v[, ptr])floatAn integer node promotes to float.
yaml.asString(v[, ptr])string
yaml.asBool(v[, ptr])bool
yaml.isNull(v[, ptr])boolWhether the addressed node is null.
yaml.asDatetime(v[, ptr])time.TimeA YAML timestamp scalar as a time.Time (needs use time;).
yaml.isDatetime(v[, ptr])boolWhether the addressed node is a timestamp.
yaml.map()yaml.ValueA fresh empty mapping, to build a document from scratch.
yaml.list()yaml.ValueA fresh empty sequence.
yaml.set(v, ptr, val)yaml.ValueUpsert a map key / replace an in-range list index.
yaml.insert(v, ptr, val)yaml.ValueInsert into a list at an index or - (end).
yaml.append(v, ptr, val)yaml.ValuePush onto the list at ptr.
yaml.remove(v, ptr)yaml.ValueRemove the addressed key / element.
yaml.move(v, from, to)yaml.ValueRelocate a subtree.

The [, ptr] argument is a JSON Pointer (see below); omit it (or pass "") to address the node itself. Every write verb is non-mutating - it returns a fresh handle, so the idiom is $v = yaml.set($v, ...), the same shape lists and maps use.

Decoding

jennifer
use io;
use yaml;

def src as string init "title: Jennifer\n"
    + "server:\n"
    + "  host: localhost\n"
    + "  ports: [8000, 8001]\n";

def doc as yaml.Value init yaml.decode($src);
io.printf("%s\n", yaml.asString($doc, "/title"));           # Jennifer
io.printf("%d\n", yaml.asInt($doc, "/server/ports/0"));     # 8000
io.printf("%s\n", yaml.asString($doc, "/server/host"));     # localhost

Scalars are typed by YAML's implicit resolution: 42 is an int (decimal, 0x hex, and 0o octal all resolve), 3.14 a float (.inf / .nan included, though yaml.asFloat refuses those: Jennifer has no non-finite float), true / false a bool, ~ / null a null, a timestamp a datetime, and everything else a string. A quoted scalar keeps its string type ("42" stays a string), and yaml.encode round-trips it faithfully - a string that reads like another type is re-quoted on output. An integer too large for a 64-bit int is kept as its exact-digit string rather than a lossy float. Mappings become maps in document order; sequences become lists; a !!binary scalar decodes to bytes. A non-scalar mapping key (a ? [a, b] complex key) and a duplicate mapping key are both rejected.

yaml.decode guards against resource-exhaustion input with normal (catchable) decode errors: a raw-text pre-scan rejects structural nesting deeper than 128 levels before the parse runs (the underlying parser is recursive, and a deeply-nested document would otherwise overflow the interpreter's stack - fatal on the fixed-stack jennifer-tiny); an input-byte cap (4 MiB on jennifer, 512 KiB on jennifer-tiny) rejects an oversized document before the backing parser materialises its whole node tree; and a node budget (shared with the other decoders) stops an alias bomb (anchors that each reference the previous one twice) from exhausting memory.

The decoded tree is the same compact, immutable one json and toml use, so binding, passing or yaml.get-ing a yaml.Value shares the tree instead of copying it. While decoding, the backing parser's own node tree also takes memory, briefly, for the whole document.

Anchors, aliases, and merge keys

An anchor (&name) and its aliases (*name) are resolved by value during decode - each alias expands to an independent copy, so the resulting tree has no shared state. A << merge key pulls another mapping's entries in; an explicit key always wins over a merged one, and an earlier merge source wins over a later one:

jennifer
def src as string init "defaults: &def\n"
    + "  retries: 3\n"
    + "  timeout: 30\n"
    + "service:\n"
    + "  <<: *def\n"
    + "  timeout: 60\n";
def doc init yaml.decode($src);
yaml.asInt($doc, "/service/retries")   # 3  (merged in)
yaml.asInt($doc, "/service/timeout")   # 60 (own key wins)

Multi-document streams

yaml.decode handles a single document and errors on a ----separated stream, pointing at yaml.decodeAll, which returns one yaml.Value per document:

jennifer
def docs as list of yaml.Value init yaml.decodeAll($stream);
for (def d in $docs) { io.printf("%v\n", yaml.keys($d)); }

JSON Pointer (RFC 6901)

YAML has no document-pointer syntax of its own, so addressing reuses json's JSON Pointer - identical /-separated syntax, so a program that walks JSON walks YAML unchanged:

jennifer
yaml.get($doc, "/server/ports/0")   # first port
yaml.has($doc, "/server/tls")       # false if absent

A pointer is "" (the whole document) or a /-led sequence of tokens; ~1 escapes a literal / inside a key and ~0 a literal ~ (apply ~1 first). List tokens are 0 or [1-9][0-9]*.

Timestamps

A YAML timestamp scalar is the one place YAML is richer than JSON. yaml.typeOf reports datetime; yaml.asDatetime returns a time.Time:

jennifer
use time;
def doc init yaml.decode("created: 2001-12-15T02:59:43Z");
def t as time.Time init yaml.asDatetime($doc, "/created");
io.printf("%s\n", time.iso($t));    # 2001-12-15T02:59:43Z

Encoding

yaml.encode renders flow style (compact, {a: 1, b: [x, y]}); yaml.encodePretty renders block style (the readable, indented form). Flow / block is YAML's own analogue of json's compact / pretty pair. A native map / list / struct / scalar encodes directly, so a yaml.Value handle is not required; a bytes value encodes as a !!binary scalar and a time.Time as a timestamp.

jennifer
def doc init yaml.decode($src);
io.printf("%s", yaml.encodePretty($doc));

Block style preserves a timestamp's implicit type on round-trip; flow style quotes a timestamp (so it round-trips as a string) - prefer encodePretty when a document carries timestamps.

Building and editing

Start from yaml.map() / yaml.list() and grow a level at a time. Writes are strict (no auto-vivification): set creates only the final pointer segment, so build intermediate maps explicitly.

jennifer
def cfg as yaml.Value init yaml.map();
$cfg = yaml.set($cfg, "/name", "demo");
$cfg = yaml.set($cfg, "/ports", yaml.list());
$cfg = yaml.append($cfg, "/ports", 8000);
$cfg = yaml.append($cfg, "/ports", 8001);
io.printf("%s", yaml.encodePretty($cfg));
# name: demo
# ports:
#     - 8000
#     - 8001

See also

  • json / toml - the sibling config libraries yaml mirrors name for name.
  • time - the time.Time yaml.asDatetime returns.
  • milestones.md - the yaml system-library design and the one place a config parser earns a Go dependency.