Implementing: Stable additive covenant
The contract: the convention author publicly commits that all future changes will be purely additive, no renames or retypes or semantic shifts. The reader trusts this and writes a single code path against the latest known schema.
Reader skeleton
def stable_additive_covenant(data):
pa = proj_attrs(data)
code = pa.get("proj:identifier") # latest known name
if code is None:
return None
# ... use the valueThat is the entire contract. No version field, no URL dispatch, no migrators, no runtime schema validation. The reader looks up fields by their current (latest-known) names and uses them. It trusts the author's covenant that the names and types and meanings will not move.
How change types hit this reader
For each of the five catalogued change types, the v1-shaped input and v2-shaped input arrive at the reader:
| before.json (v1) | after.json (v2) | What happens | |
|---|---|---|---|
| add-optional | OK | OK | The reader's current understanding of every field still matches both shapes. New optional field is harmless. |
| rename | SILENT-WRONG | OK | v1 carries the old name proj:code. The reader looks up proj:identifier (its current understanding), finds nothing, and silently returns None. |
| retype | SILENT-WRONG | OK | v1 carries proj:code as a string. The reader assumes the v2 shape (array of integer) and pulls element [0], which on a string gives the character 'E'. The reader proceeds with the wrong value. |
| semantic-only | SILENT-WRONG | OK | The JSON is structurally identical to v2 data. The reader applies its current (v2) interpretation. v1 data is interpreted as v2 and produces wrong coordinates. |
| add-with-semantic-shift | OK | SILENT-WRONG | v1 has no signal of the new field; reader's baked-in semantics match v1 reality. v2 carries proj:cell_alignment: "corner" which the reader does not know to look up; it applies the same baked-in semantics, which is wrong for v2 data. |
Four of five rows produce a silent failure. The reader returns a value that looks plausible but is wrong. There is no detection signal in the document or in the reader code: that is the cost of trusting the covenant.
When the author breaks the covenant
The covenant says: no renames, no retypes, no removes, no semantic shifts; only additive changes. What does each violation look like from the reader's side?
- Author renames a field (e.g.,
proj:code→proj:identifier). The reader looks up the new name and gets nothing. Silently missing data. No signal. - Author retypes a field (e.g.,
proj:codefrom string to array). The reader assumes the new shape and uses old data as the new shape: indexing a string where an array was expected pulls a character; arithmetic on the wrong type may crash deep in downstream code or silently produce garbage. No signal at the reader level. - Author semantically shifts an existing field (e.g., re-anchors transform coefficients). JSON is unchanged. The reader applies its current understanding to old data, producing wrong output with no signal.
- Author adds a field whose presence changes interpretation of an existing field (the "cell-type" case). Old documents lack the field, default interpretation matches old reality, so old documents are fine. New documents carry the new field; the reader ignores it under the safely-ignorable principle and applies the wrong (default) interpretation.
All four violations are silent. The covenant only works if the author honors it; the reader has no machinery to detect breaches.
If you do add JSON Schema validation
A reader that validates incoming documents against a frozen JSON Schema can catch some violations loudly: a retype becomes FAIL: schema mismatch instead of garbage downstream. Validation is opt-in and most JSON-metadata consumers do not include it (see the survey on real-world implementer practice); adding it is a useful belt-and-suspenders mitigation when the cost of silent failure is high.
Run it
uv run walkthroughs/01-stable-additive-covenant/run.pyOutcome matrix
before.json after.json
01-add-optional OK OK
02-rename SILENT-WRONG: identifier=None OK
03-retype SILENT-WRONG: epsg='E' OK
04-semantic-only SILENT-WRONG: corner=(500000.0, 5000000.0) OK
05-add-with-semantic-shift OK SILENT-WRONG: corner=(499995.0, 5000005.0)The SILENT-WRONG rows print the value the reader actually produced. epsg='E' on the retype row is the single character pulled when the reader treated the legacy string "EPSG:4326" as the array shape v2 expected: code[0] returned the literal letter 'E', and the reader passed it downstream with no signal that anything was wrong.
Reader code
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
"""Implementer's walkthrough for Contract 1: Stable additive covenant.
Illustrative only. Not for production use.
A single reader is implemented against the latest known understanding of
the convention. The reader trusts the convention author's promise of
purely additive evolution: it looks up fields by their current names,
uses the values directly, and ships no runtime schema validation. This
mirrors how most consumers of JSON metadata conventions actually work.
"""
import json
import sys
from pathlib import Path
HERE = Path(__file__).parent
FIXTURES = HERE.parent / "fixtures"
CHANGE_TYPES = [
"01-add-optional",
"02-rename",
"03-retype",
"04-semantic-only",
"05-add-with-semantic-shift",
]
INPUTS = ["before.json", "after.json"]
EXPECTED_MATRIX = {
"01-add-optional": {"before.json": "OK", "after.json": "OK"},
"02-rename": {"before.json": "SILENT-WRONG: identifier=None", "after.json": "OK"},
"03-retype": {"before.json": "SILENT-WRONG: epsg='E'", "after.json": "OK"},
"04-semantic-only": {"before.json": "SILENT-WRONG: corner=(500000.0, 5000000.0)", "after.json": "OK"},
"05-add-with-semantic-shift": {"before.json": "OK", "after.json": "SILENT-WRONG: corner=(499995.0, 5000005.0)"},
}
def load(change: str, name: str) -> dict:
return json.loads((FIXTURES / change / name).read_text())
def _proj_attrs(data: dict) -> dict:
return {k: v for k, v in data.get("attributes", {}).items() if k.startswith("proj:")}
def _corner_center_anchored(t: list[float]) -> tuple[float, float]:
return (t[2] - t[0] / 2.0, t[5] - t[4] / 2.0)
def _corner_corner_anchored(t: list[float]) -> tuple[float, float]:
return (t[2], t[5])
def _correct_corner(data: dict, change: str) -> tuple[float, float]:
"""Ground truth: what the document was written to mean, by version."""
pa = _proj_attrs(data)
t = pa.get("proj:transform")
if t is None:
return (0.0, 0.0)
url = data["attributes"]["zarr_conventions"][0].get("schema_url", "")
if change == "05-add-with-semantic-shift":
return _corner_corner_anchored(t) if pa.get("proj:cell_alignment", "center") == "corner" else _corner_center_anchored(t)
if change == "04-semantic-only":
return _corner_center_anchored(t) if "/v1/" in url else _corner_corner_anchored(t)
return (0.0, 0.0)
def stable_additive_covenant(data: dict, change: str) -> str:
"""Latest-understanding reader. No runtime validation, no version dispatch.
Looks up fields by their current names and applies the current interpretation.
"""
pa = _proj_attrs(data)
# 02-rename: look up the field by its current (v2) name.
if change == "02-rename":
identifier = pa.get("proj:identifier")
if identifier is None:
return f"SILENT-WRONG: identifier={identifier}"
return "OK"
# 03-retype: assume proj:code is the v2 shape (array of integer).
# A naive reader pulls element [0] without type-checking.
if change == "03-retype":
code = pa.get("proj:code")
epsg = code[0]
if not isinstance(epsg, int):
return f"SILENT-WRONG: epsg={epsg!r}"
return "OK"
# 04, 05: extract proj:transform and compute the corner using the reader's
# current (v2) interpretation. v1 data interpreted with v2 semantics
# silently produces the wrong corner.
if change == "04-semantic-only":
got = _corner_corner_anchored(pa["proj:transform"])
if got != _correct_corner(data, change):
return f"SILENT-WRONG: corner={got}"
return "OK"
if change == "05-add-with-semantic-shift":
# Reader is at v1-knowledge: doesn't look up the new proj:cell_alignment
# field, so it always applies center-anchor semantics.
got = _corner_center_anchored(pa["proj:transform"])
if got != _correct_corner(data, change):
return f"SILENT-WRONG: corner={got}"
return "OK"
# 01-add-optional: just use proj:code.
code = pa.get("proj:code")
if code is None:
return "SILENT-WRONG: code missing"
return "OK"
def print_matrix(matrix: dict[str, dict[str, str]]) -> None:
col_w = max(len(c) for c in CHANGE_TYPES) + 2
val_w = max(max(len(v) for v in row.values()) for row in matrix.values()) + 2
header = " " * col_w + " ".join(f"{i:<{val_w}}" for i in INPUTS)
print(header)
for c in CHANGE_TYPES:
row = f"{c:<{col_w}}" + " ".join(f"{matrix[c][i]:<{val_w}}" for i in INPUTS)
print(row)
def main() -> int:
matrix: dict[str, dict[str, str]] = {}
for change in CHANGE_TYPES:
matrix[change] = {}
for name in INPUTS:
data = load(change, name)
matrix[change][name] = stable_additive_covenant(data, change)
print_matrix(matrix)
if matrix != EXPECTED_MATRIX:
print("\nFAIL: matrix does not match documented expectation.", file=sys.stderr)
return 1
print("\nOK: matrix matches documented expectation.")
return 0
if __name__ == "__main__":
raise SystemExit(main())Commentary
The Stable additive covenant is the simplest contract to implement and the most exposed if the author breaks it. Four of five change types produce silent failures: the reader returns plausible-looking output with no signal that the underlying assumption was violated. The contract is a forcing function on the author: it gives the reader no machinery to detect a misstep, which is why the survey found GeoJSON's no-version covenant broke when the crs member was removed in 2016.
If you are choosing this contract: be confident the author will hold the covenant for the lifetime of the data, and reach for a stronger contract (URI dispatch or Integer-major) if you cannot.