Skip to content
zabloo

Loading

What an SDK does with a payload it was not built for: three levels of diagnostic, a repaired result rather than a refusal, the stable code table, and forward-tolerance.

A game ships in March. In September its shop screen gets new content pushed to it — but the copy of the SDK inside that game is still the March one. This page is what happens when the two do not quite match, or when the download simply arrived broken: the SDK says what it found, repairs what it can, and renders the rest. A corrupt envelope must never take the game down.

That is the whole promise, and the rules below are how it is kept. Content is delivered to live games and hot-updated independently of the SDK, so an SDK will eventually be handed a payload it was not built for — newer, older, truncated by a bad download, or simply wrong.

The three pages that explain hot-update

Shipping UI to a game that is already in players’ hands is what this format exists for, and three pages carry it between them. Versioning says whether an SDK and a payload can meet at all. This page says what the SDK does with the payload once they do — what it repairs, what it refuses, and what it tells the game. The host channel is the call that hands the new payload over, reload, and the callback the diagnostics come back on.

The policy below is one policy, implemented once in @zabloo/format (readEnvelope) and ported literally by every SDK. Both targets degrade the same way in front of the same bytes, and a consumer that loaded an envelope can trust its shape instead of defending itself at every node, every frame.

Three levels

Not everything wrong with a payload is equally wrong. A missing colour token is a typo the screen can survive; a truncated file is not a screen at all. Sorting findings into three levels is what lets a game keep playing through the first and hear about the second, and every SDK sorts them identically.

The line between them is one question: is there still a tree to render?

normativeDiagnostic levels

LevelWhat it covers
fatalInvalid or truncated JSON, not an object, a missing or non-numeric v, an incompatible major version, a missing views map, and zero usable views once everything else is repaired.
warnEverything repairable locally: a view that is not a node, a malformed node, a prop of the wrong type, an invalid asset entry, a dangling token/asset/anchor reference, a duplicate id, a subtree nested too deep. The envelope loads without them.
silenceUnknown properties and unknown node types. Forward-tolerance is a feature, not an error.

Shapes, never vocabularies

A closed set — Easing, ImageFit, AnchorAt, GroupBehavior, ScrollAxis — is checked to be a string, and no further.

The vocabulary is exactly what a later version grows, and every consumer already falls back to its default on a value it does not know. Validating the value here would turn tomorrow’s content into today’s error, which is the opposite of what the validator is for.

The result is repaired, not just reported

readEnvelope returns a copy with the broken parts removed. The caller’s object is never mutated, and unknown properties survive the copy untouched.

Two repairs are worth knowing about:

  • A dropped positional slot is replaced by an inert empty Container. Collapse, Toggle, Slider, ProgressBar and Repeat read their children by position, so removing a broken one would renumber the rest and silently change what they mean. A placeholder keeps the numbering honest.
  • A prop of the wrong type falls back to its default rather than dropping the node.

The read is also depth-capped at 256 levels. Nothing authored comes close, but every pass downstream — validation, layout, paint, hit-testing — is recursive, so a tree that would overflow the stack stops being a tree at the door. The cut is an ordinary warning: that subtree is dropped, the rest of the UI loads.

Diagnostics

A diagnostic is what the SDK hands back after reading a payload: one object per thing it found. Tooling matches on the code and shows the message, so the code is the part that never changes.

Every finding carries a stable code — that is the contract, not the prose of the message — plus a path into the envelope (views["hud"].children[2].text), and a self-contained human-readable message naming the field and the reason. Map keys are bracketed because view ids, asset ids and token names contain dots of their own.

normativeDiagnostic codes

CodeLevelMeaning
invalid-jsonfatalThe payload is not parseable JSON.
not-an-objectfatalThe payload is not an object.
missing-versionfatalNo v, or v is not a number.
unsupported-versionfatalThe major version is one this reader does not implement.
missing-viewsfatalNo views map.
no-usable-viewsfatalEvery view was dropped during repair.
invalid-tokenswarnThe token dictionary is not an object.
invalid-tokenwarnA token value is neither a string nor a number.
invalid-assetswarnThe asset manifest is not an object.
invalid-assetwarnAn entry is missing hash/mime/size, or data is not base64.
invalid-nodewarnA node is not an object, or has no usable type.
invalid-propwarnA property has the wrong type; it falls back to its default.
invalid-bindingwarnA binding's path is malformed.
too-deepwarnThe subtree exceeds the depth cap.
duplicate-idwarnTwo nodes in a view share an id.
unknown-tokenwarnA {token} the dictionary does not define.
unknown-assetwarnAn asset: ref with no manifest entry.
unknown-anchorwarnAn overlay anchor id that matches no node.

Asset entries are checked by shape onlydata is never decoded during validation, since that would pay the cost twice.

What consumers do with it

  • Mounting throws on a fatal diagnostic. There is no previous UI to protect, and the caller has to hear that its payload never became a view. The error message is the fatal diagnostic’s, and it carries the warnings found on the way there.
  • Reloading never throws. A hot-update the validator refuses is reported and discarded: the envelope on screen stays exactly as it is. A bad update costs the player an update, never their session.
  • Export validates before writing. A fatal diagnostic aborts the export; warnings go into its summary. What it writes is the author’s tree, never the repaired one — silently dropping a node from the artifact would hide the bug the warning just named.
  • Where they are reported is up to whoever mounted the view. The web renderer takes an onDiagnostic callback, on mount and reload alike, that receives these objects — code, path and all — so an error overlay, a dev server or an editor can show them where the author is looking. With no callback they go to the console.

Warnings are emitted once, at load, not per frame.

Running the contract yourself

zabloo validate [file] applies exactly this policy to an envelope on disk and reports it as an exit code: 0 when an SDK would load it, 1 on a fatal, and --strict to fail on the repaired warnings too. --json gives the diagnostics as values — level, code, path, message — so a CI step can annotate the diff instead of printing a line.

npx zabloo validate --strict

The envelope is a payload delivered to live games and hot-updated into them, so this is the check worth running at the pull request rather than at the player.

Forward-tolerance

This is the rule that lets the format grow at all. A game running the March SDK will be handed content authored against a later one, and it has to render the parts it understands rather than give up — so what it does with the parts it does not is spelled out here, not left to each target.

normative

What an SDK does with content built for a newer version of the format:

SituationBehaviour
Unknown propertyIgnored, silently. It survives a validation round-trip untouched.
Unknown node typeRendered as a Container, preserving layout, style, visible, disabled and children.
Unknown value in a closed setFalls back to that property's default.
Unknown group behaviourIgnored — the children lay out as ordinary siblings.
Incompatible major versionRefused: unsupported-version, fatal.
Why the unknown-type rule is the whole growth story

A new primitive lands in an old SDK as a plain box holding its children: the layout survives, only the new capability is missing. It is also why every primitive is designed so that its Container degradation is a reasonable picture of it — a Repeat becomes one static copy of its template, a ProgressBar its track with an unsized fill, a Spinner its beads at rest.

See Versioning for which changes are allowed to rely on this and which ones are not.