Skip to content

Implementing: Structural inspection

The contract: the convention has no version field. Identity is the JSON shape itself. The reader recognizes the convention's data by a structural signature (here, the presence of proj:code as a string) and applies its baked-in interpretation. No schema, no URL dispatch, no version awareness.

Reader skeleton

python
def structural_inspection(data):
    pa = proj_attrs(data)
    if "proj:code" not in pa:
        return "FAIL: unrecognized structure"
    if not isinstance(pa["proj:code"], str):
        return "FAIL: unrecognized type"
    code = pa["proj:code"]
    # ... apply baked-in interpretation

The "signature" is hand-written: a couple of isinstance checks the implementer added because they know what shape they expect. This is not JSON Schema validation; it is the same code the implementer would write if the spec had no schema at all. The signature is frozen once the reader ships; the implementer has to go edit it to extend it.

How change types hit this reader

before.json (v1)after.json (v2)What happens
add-optionalOKOKSignature still matches both versions; additive fields are tolerated under the safely-ignorable principle.
renameOKFAILv2 dropped proj:code for proj:identifier. Signature no longer recognizes v2 data. Loud failure.
retypeOKFAILv2 changed proj:code to an integer array. Signature requires a string; the type check fails. Loud failure.
semantic-onlyOKSILENT-WRONGThe JSON shape is unchanged across the version transition. The reader recognizes the structure and applies its v1-knowledge interpretation; v2 data carries v2 semantics that the reader does not implement.
add-with-semantic-shiftOKSILENT-WRONGSame shape recognized. v2 carries proj:cell_alignment: "corner" which the reader does not know to inspect. It applies its v1 (center) interpretation.

Structural inspection turns a JSON-shape break into a loud failure (rename and retype catch loudly). Semantic-only shifts are invisible to it: the signature cannot distinguish v1 data from v2 data when only the meaning changed.

When the author breaks the covenant

The Structural inspection contract has a stricter version of the additive covenant: the author promises both no semantic shifts and no signature-breaking changes. Each violation has a predictable shape:

  • Author renames a field (signature-breaking). Reader fails loudly on documents written under the new name. Old data still reads fine. Loud failure on one side of the transition.
  • Author retypes a field (signature-breaking). Same as above; the type check rejects new-shaped data.
  • Author removes a field that was part of the signature. Reader fails to recognize new documents at all. Loud.
  • Author semantically shifts an existing field (signature-preserving). No detection mechanism. Silent misread on whichever side of the shift the reader was not coded against.
  • Author adds a field whose presence changes interpretation of an existing field (signature-preserving). Same: the new field is unknown to the reader, which ignores it under the safely-ignorable principle and applies the original interpretation regardless of the new field's value.

Structural inspection is sturdier than Stable additive covenant for JSON-shape breaks (it catches them loudly), but no better for semantic shifts. The two contracts have the same Achilles heel: the survey lists 8 catalogued cases of semantic-only or add-with-semantic-shift that this contract cannot detect.

Run it

bash
uv run walkthroughs/02-structural-inspection/run.py

Outcome matrix

                            before.json    after.json
01-add-optional             OK             OK
02-rename                   OK             FAIL: unrecognized structure
03-retype                   OK             FAIL: unrecognized type
04-semantic-only            OK             SILENT-WRONG: corner=(499995.0, 5000005.0)
05-add-with-semantic-shift  OK             SILENT-WRONG: corner=(499995.0, 5000005.0)

Reader code

python
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
"""Implementer's walkthrough for Contract 2: Structural inspection.

Illustrative only. Not for production use.

The reader recognizes the convention by inline type-aware shape checks
(here, `isinstance(pa["proj:code"], str)`). This is not JSON Schema
validation: the implementer hand-wrote the shape expectation in the
recognition path. No version field, no URL dispatch. The recognized
shape is whatever the implementer coded against, here v1.
"""

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": "OK", "after.json": "FAIL: unrecognized structure"},
    "03-retype":                  {"before.json": "OK", "after.json": "FAIL: unrecognized type"},
    "04-semantic-only":           {"before.json": "OK", "after.json": "SILENT-WRONG: corner=(499995.0, 5000005.0)"},
    "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 _correct_corner(data: dict, change: str) -> tuple[float, float]:
    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 (t[2], t[5]) 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 (t[2], t[5])
    return (0.0, 0.0)


def structural_inspection(data: dict, change: str) -> str:
    """Recognize the convention by hand-written shape checks
    (isinstance on proj:code), then apply v1-knowledge interpretation.
    """
    pa = _proj_attrs(data)

    # Hand-written signature check: presence and string type of proj:code.
    if "proj:code" not in pa:
        return "FAIL: unrecognized structure"
    if not isinstance(pa["proj:code"], str):
        return "FAIL: unrecognized type"

    # Recognition passed. Apply baked-in (v1-knowledge) interpretation.
    if change in {"04-semantic-only", "05-add-with-semantic-shift"}:
        got = _corner_center_anchored(pa["proj:transform"])
        expected = _correct_corner(data, change)
        if got != expected:
            return f"SILENT-WRONG: corner={got}"

    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] = structural_inspection(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

Structural inspection is the "JSON structure plus fail-on-unknown is sufficient" position from zarr-conventions-spec#7. For specs that never change semantics (only their shape), it is workable. For specs that do shift semantics, it has no signal to dispatch on. The survey's eight semantic-only / add-with-semantic-shift cases are the documented falsifiers.

If you choose this contract, your reader will be sturdy against rename and retype but blind to the very change-types that motivate strong versioning. Reach for URI dispatch or stronger if your domain admits any chance of meaning drift.