Skip to content

Support matrix: widen the refused JSON Schema keywords (patternProperties, if/then/else, prefixItems, contains, dependent*, unevaluated*) #157

Closed

Description

@rasuvaeff

Summary

Twelve JSON Schema assertion keywords are refused at compile time as "outside the v0.1 support matrix" (SchemaCompiler::UNSUPPORTED_ASSERTION_KEYWORDS):

contains, contentSchema, dependentRequired, dependentSchemas, if/then/else, patternProperties, prefixItems, propertyNames, unevaluatedItems, unevaluatedProperties.

The backend (opis/json-schema 2.x) evaluates every one of them under draft 2020-12; the refusal is this package's own, because two things of ours do not reach them yet:

  1. the directional readOnly/writeOnly rewrite (SchemaCheck::effective()) recurses through properties, items, additionalProperties and the composition keywords only — a schema under patternProperties or then would be judged without the rewrite, i.e. wrongly in one direction;
  2. rasuvaeff/property-testing-openapi has no generator for them, and the two packages move together.

Fail-closed was the right call for 0.1; a document that uses patternProperties for a map of ^x- keys, or if/then/else for a conditional required, is ordinary OpenAPI today and is currently unloadable here.

Proposal

Widen the matrix one keyword (group) at a time, each step being: the rewrite reaches the keyword's subschemas in both directions (with a property test that effective() stays a fixed point and dialect-independent) → the keyword leaves the refusal list → a generator lands in property-testing-openapi → the recorded corpus (bin/record-openapi-corpus) is re-recorded.

Suggested order, by how often the keyword appears in real documents:

  1. patternProperties (+ propertyNames)
  2. if / then / else
  3. prefixItems / contains (arrays as tuples)
  4. dependentRequired / dependentSchemas
  5. unevaluatedProperties / unevaluatedItems — last: they read annotations from sibling applicators, so the rewrite has to be provably annotation-neutral; keep refusing until that is shown.

contentSchema stays refused: it applies to a string the validator would have to decode by contentMediaType first, and non-JSON payloads are out of scope.

Context

Comparison with duyler/openapi (2026-09-20), which evaluates all of these at runtime; the gap is the one substantive feature difference against this package's fail-closed design. Compatibility: each step turns an InvalidContract into a loadable document, so it is additive — minor releases.

Activity

  1. self-assigned this
    on Sep 19, 2026
  2. rasuvaeff commented on Oct 11, 2026

    @rasuvaeff
    OwnerAuthor

    Steps 1 and 2 of the proposed order are done.

    1. patternProperties (+ propertyNames) — merged: openapi-contract#171, property-testing-openapi#140.

    2. if / then / else — open: openapi-contract#172, property-testing-openapi#141.

    On the contract side each of the three becomes the single subschema it is: compiled instead of refused, walked by the load-time shape check, parsed eagerly by assertParsed(), evaluated by the backend under draft 2020-12 at any depth and nested inside one another. A boolean if/then/else stays refused under OAS 3.0, which predates boolean schemas. The directional rewrite reaches all three branches — the property test was checked against the three mutants it exists for, dropping if, then or else from the walk each fails it.

    On the generator side each reachable branch is built constructively: then as a conjunction of the base schema, the condition and the branch, so the condition holds by construction; else from the base schema and else, filtered by the condition the contract's own SchemaCheck judges. The zoo gained shipments.create and the corpus is re-recorded — --check against the previous fixture reported only the new cases, no moved verdicts.

    Remaining, in the proposed order: prefixItems/contains, then dependentRequired/dependentSchemas, then unevaluated* last. contentSchema stays refused.

  3. added a commit that references this issue on Oct 11, 2026
  4. rasuvaeff commented on Oct 11, 2026

    @rasuvaeff
    OwnerAuthor

    Step 2 is merged: openapi-contract#172 and property-testing-openapi#141.

    Contract side, per keyword: each of if, then, else becomes the single subschema it is — compiled, walked by the load-time shape check, parsed eagerly, evaluated at any depth and nested inside one another, with a boolean form refused under OAS 3.0 as items and not already are. The directional rewrite reaches all three branches; the property test fails if any one of them is dropped from the walk (checked against all three mutants).

    Generator side: each reachable branch is built constructively and checked by the contract's own SchemaCheck through the same JsonBodyEncoder the materializer uses. shipments.create in the zoo covers both sides; the corpus is re-recorded, and --check against the previous fixture reported only the new cases, no moved verdicts.

    Next is step 3, prefixItems / contains. Two decisions to make there rather than discover mid-change: whether minContains / maxContains are read alongside contains (the backend applies them; refusing contains while reading nothing is silent loss, and so is reading contains while dropping the bounds), and — for the generator — that a contains over an array needs at least one element drawn from the subschema rather than a filter that exhausts.

  5. rasuvaeff commented on Oct 11, 2026

    @rasuvaeff
    OwnerAuthor

    The remaining support-matrix work is now prepared as one minor-release wave:

    The wave adds prefixItems/contains with minContains/maxContains, dependentRequired/dependentSchemas, and unevaluatedProperties/unevaluatedItems. contentSchema remains refused. Both packages pass composer build; merge #173 before #142 so the generator's development-contract CI sees the new contract support.

  6. rasuvaeff commented on Oct 11, 2026

    @rasuvaeff
    OwnerAuthor

    Готово. Реализовано и смержено в два PR:\n\n- #173 — валидация оставшихся ключевых слов схемы (, //, , , , ).\n- rasuvaeff/property-testing-openapi#142 — генерация для этих ключевых слов в property-testing-openapi.\n\nПоддержка в генераторе конструктивная; остаётся fail-closed ( для неподдерживаемых комбинаций). Корпус — 540 кейсов.\n\nПопутно исправлен реальный дефект: значения для компилировались один раз на весь объект, а соединял имена и значения независимо, поэтому значение, скомпилированное под одно имя, могло попасть под другое. Имена и значения теперь генерируются вместе ( по сгенерированному имени); до фикса валидатор отклонял 337 из 500 объектов, после — 0.\n\nВесь CI зелёный, включая Psalm, Rector и гейт мутации (MSI 91.14% при пороге 91).

  7. rasuvaeff commented on Oct 11, 2026

    @rasuvaeff
    OwnerAuthor

    Готово. Реализовано и смержено в два PR:

    Поддержка в генераторе конструктивная; contentSchema остаётся fail-closed (UnsupportedGeneration для неподдерживаемых комбинаций). Корпус — 540 кейсов.

    Попутно исправлен реальный дефект: значения для patternProperties компилировались один раз на весь объект, а dictOf соединял имена и значения независимо, поэтому значение, скомпилированное под одно имя, могло попасть под другое. Имена и значения теперь генерируются вместе (by: по сгенерированному имени); до фикса валидатор отклонял 337 из 500 объектов, после — 0.

    Весь CI зелёный, включая Psalm, Rector и гейт мутации (MSI 91.14% при пороге 91).

  8. added a commit that references this issue on Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions