Repository navigation
softschema 0.7.0
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$refbranches remain
unchanged internally; their parent receives annotation-awareunevaluatedProperties: false. This preserves alternative branch selection and successful-branch annotations.
Direct lexical objects continue to useadditionalProperties: 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 acceptstatusand offlineresources. Existing model-only calls retain
their semantic-only behavior, including understatus: 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: enforcedsupport 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 returnenforcement_unsupportedwith stable
reason,schema_path, andmessagefields. Malformed graphs remainschema_invalid.
Fixes and hardening
- Unsafe
anyOf,oneOf, and$reftransformations 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
additionalPropertiesor
unevaluatedPropertiesreceive no redundant closure keyword, which keeps the common
generatedanyOf: [$ref, null]nullable-field shape valid when its object graph is
already explicit. - Definition closure is reference-aware, and
notis no longer treated as a declaration. - Structured
itemsand disjointprefixItems/itemsreceive the rule independently,
whilecontainsremains 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