Skip to content

softschema 0.7.0

Choose a tag to compare

@jlevy jlevy released this 25 Aug 07:30
e014e5b

softschema 0.7.0 makes status: enforced usable on composed and conditional schemas.
Shapes that 0.6.2 refused outright — allOf, if/then/else, dependentSchemas,
anyOf, oneOf, and $ref object graphs — now return real document verdicts instead of
failing before the document was examined. Ordinary schema verdicts are unchanged; the
breaking surface is the shape of structural error records, so this is a minor release.

What's Changed

Breaking changes and migration

Structural errors now identify both a stable category and, for field-level repairs, the
affected property. The verdict for a given document is unchanged — what changed is the
record describing it:

0.6.2:  validator: "additionalProperties"   message: "object has properties that are not allowed"
0.7.0:  validator: "unevaluatedProperties"  message: "property 'surprise' is not allowed"
        + code: "undeclared_property"       + property: "surprise"

Consumers that match engine keywords or assume one aggregate record per object should
migrate:

Before After
Match validator == "additionalProperties" Match code == "undeclared_property"; composed sites report unevaluatedProperties
Match {kind, code, path} for a field repair Match {kind, code, path, property}
Read one generic missing/extra record Read one record per affected field, with a property-specific message
Treat every enforcement_unsupported as composed-schema refusal Inspect its stable reason; only shapes outside the support matrix are refused

The code values are undeclared_property, missing_property, invalid_value, and
unmapped_keyword. validator, validator_value, and value remain diagnostic fields,
not the field-repair match surface.

Callers that supply external resources must key each one by an absolute URI without a
fragment, and a resource root $id, when present, must resolve to that key. This makes
Python and TypeScript resolve the same fully supplied offline resource graph rather than
relying on engine-specific retrieval behavior. Ordinary callers that pass only a compiled
schema are unaffected.

TypeScript array positions in structural error paths are now numeric, matching Python.

New features

  • Supported composed object schemas validate under enforced (#41). allOf, anyOf,
    oneOf, if/then/else, dependentSchemas, and supported $ref branches remain
    unchanged internally; their parent receives annotation-aware unevaluatedProperties: false. This preserves alternative branch selection and successful-branch annotations.
    Direct lexical objects continue to use additionalProperties: false. The offline graph
    supports local pointers, escaped tokens, anchors, nested definitions, embedded $id
    resources, supplied resources, and literal or pattern-based declarations.

  • Values APIs can request the checked structural policy. Both Python and TypeScript
    values APIs accept status and offline resources. Existing model-only calls retain
    their semantic-only behavior, including under status: enforced; the status changes
    structural behavior only when a schema is supplied.

  • Field-level structural diagnostics identify the repair target. Missing and
    undeclared-property errors name the field and preserve one record per affected field.

  • An explicit status: enforced support matrix. Dynamic references, unsafe nested
    instance composition, conditionals whose matcher annotations escape the unconditional
    declaration scope, directly applied structured embedded resources, and references to
    directly applied non-reusable targets return enforcement_unsupported with stable
    reason, schema_path, and message fields. Malformed graphs remain schema_invalid.

Fixes and hardening

  • Unsafe anyOf, oneOf, and $ref transformations that could change the authored
    schema's result are corrected. Sibling child evaluators and context-sensitive
    composition references are refused when inserting undeclared-property rejection could
    change intersection, branch-selection, or conditional-success semantics.
  • Pure references to targets that already state additionalProperties or
    unevaluatedProperties receive no redundant closure keyword, which keeps the common
    generated anyOf: [$ref, null] nullable-field shape valid when its object graph is
    already explicit.
  • Definition closure is reference-aware, and not is no longer treated as a declaration.
  • Structured items and disjoint prefixItems/items receive the rule independently,
    while contains remains an unchanged matcher.
  • Python derives missing required fields from validator data rather than English error
    text.

Compatibility

Compiled schemas and schema_sha256 are unchanged, because the checked overlay remains
validation-time only. The supported package-root exports are unchanged. Explicit
additionalProperties or unevaluatedProperties at an instance site still wins, and
mappings with no reachable declaration remain open. Model-only and metadata-only
validation keep their 0.6.2 verdicts and skip reasons, so this release does not require
existing hosts to add structural schemas.

Documents previously refused solely because they used supported composition now report
their real valid or invalid outcome. Because 0.6.2 failed those before examining the
document, that change moves results from red to green rather than the reverse. A schema
outside the supported matrix fails before document validation with an actionable reason
rather than receiving a partial overlay. See the spec's
support matrix
for the exact boundary and author workarounds.

Documentation

The composition and closure material is now explicitly marked as advanced wherever it
appears. An ordinary schema — one that declares its fields in one place per object — needs
none of it, and the README, guide, and spec each say so and say what can be skipped. The
two triggers that do require it are named consistently: declarations composed across
allOf/anyOf/oneOf/$ref, and dependent schemas where one field's schema is selected
by another field's value. The research brief deriving the annotation model is linked for
depth from the spec, the cross-field playbook, and both Further Reading lists.

Testing and release safety

The enforcement work is owned by the shared golden corpus, which runs against Python,
Node, and Bun, and by shared raw-versus-enforced parity vectors covering alternatives,
references, resources, patterns, conditionals, unsupported boundaries, and field-error
multiplicity. The few accepted jsonschema/Ajv record-set differences are pinned exactly
by engine_deviations vectors; validation verdicts for the supported matrix remain equal
across runtimes.

Before tagging: lint, both unit suites, the golden corpus on all three runtimes,
cross-implementation parity, both package builds, publint, and Markdown format checks.
A direct 0.6.2-versus-0.7.0 comparison confirmed that ordinary flat-schema verdicts are
identical and that all nine user-facing enforcement_unsupported reasons agree between
the two implementations and are documented in the spec.

Full Changelog: v0.6.2...v0.7.0