Skip to content

fix(spec): keep the stricter constraint when 3.0 conversion folds overlapping keywords - #873

Merged
yeongseon merged 7 commits into
mainfrom
fix/spec-868-overlapping-keywords
Oct 30, 2026
Merged

yeongseon merged 7 commits into
mainfrom
fix/spec-868-overlapping-keywords

Conversation

@yeongseon

@yeongseon yeongseon commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

JSON Schema 2020-12 lets a schema carry keywords that overlap, such as minimum with exclusiveMinimum, enum with const, and prefixItems with items. The OpenAPI 3.0 conversion folded each pair by keeping one side, so the converted schema could accept values the original rejects. This keeps both constraints.

Changes

  • Numeric exclusiveMinimum / exclusiveMaximum next to an inclusive bound: the stricter of the two is kept, and equal bounds stay exclusive. Pydantic emits both for Field(ge=5, gt=1); the 3.0 output used to loosen that to minimum: 1 with an exclusive flag.
  • const next to enum: the two are intersected using JSON Schema equality, so a boolean is never equal to 1 and 1.0 equals 1, including inside lists and objects. A const that is in the enum narrows it to that value; one that is not leaves a schema that accepts nothing, expressed as an extra allOf member. An empty enum, which OpenAPI 3.0 does not allow, becomes not: {} instead.
  • prefixItems next to items: items: false now caps maxItems at the prefix length (a tighter existing maxItems is kept). An items schema that equals every prefix schema collapses to a plain items; a different one joins the existing items.anyOf approximation and is reported through the same positional-loss warning (an error under strict=True) as a heterogeneous tuple. A tail that a maxItems at or below the prefix length makes unreachable is ignored.
  • docs/configuration.md describes the rules. 3.1 and 3.2 output is unchanged.

Validation

  • make check-all on 2f074c2: exit 0, 2116 passed, 30 skipped, coverage 96.40% (gate: 95%). The later commit fd8b6ef changes one documentation sentence.
  • A differential run with the reference validators (OAS31Validator on the original, OAS30Validator on the converted schema), covering positive, negated, property, array-item and combinator positions, inline and as a component: 252,006 comparisons across 4,028 generated documents with no mismatch that is not reported through a warning or a strict-mode error. Re-run on 2f074c2.
  • Reverting each code change makes new tests fail, for example 14 failures for the empty-enum handling and 6 for the tail reachability check.
  • GitHub CI on the final head is pending when this is written.

Compatibility

  • Only OpenAPI 3.0 output changes, and only for schemas that carry these overlaps. The new schemas accept exactly what the original does, so 3.0 clients may see tighter bounds than before.
  • A prefixItems schema whose items differs from its prefix and can be reached is now reported with a version-downgrade-drop warning; that warning gates --fail-on-warnings and raises under strict=True. Until now the tail was dropped without a message.
  • Known limit, tracked in fix(spec): keep the surrounding schema's keys when 3.0 conversion inlines a nullable reference #870: a nullable reference to a component that accepts nothing (a const outside its enum, or an empty enum) rejects null, because the nullable-reference path does not yet reach allOf members. On main that input wrongly accepted the const value instead.

Related Issues

Closes #868

yeongseon and others added 4 commits October 30, 2026 23:10
Preserve inclusive bounds that are tighter and make equal numeric bounds exclusive. Cover inline and component output and the Pydantic producer path.

Refs #868

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
Reuse singleton intersection for null-only schemas and compare JSON values recursively without equating booleans and numbers. Preserve conflicting predicates in allOf.

Refs #868

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
Cap closed prefixes at their position count and retain a tighter maxItems. Include schema tails when detecting homogeneous items or reporting the existing positional-loss approximation.

Refs #868

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
Document stricter numeric bounds, enum/const intersection, closed-prefix caps and positional-loss reporting for differing schema tails.

Refs #868

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Schema conversion still broadens capped arrays with unreachable tail types and incorrectly rejects null for nullable references with generated allOf constraints without a loss warning.

2 open issues
What changed in this PR

This PR tightens OpenAPI 3.0 schema conversion to preserve overlapping JSON Schema constraints, addressing #868. OpenAPI 3.1 and 3.2 output remains unchanged.

Changes:

  • Keeps stricter numeric bounds and intersects enum with const using JSON Schema equality.
  • Preserves closed-prefix array limits and includes schema tails in positional-loss handling.
  • Adds regression coverage and documents the conversion rules.
File Description
tests/​test_spec_conversion_characterization.py Adds overlap, validation-equivalence, and positional-loss regression cases.
src/​azure_functions_openapi/​_schema_version_conversion.py Updates bound, enum/const, and prefix/tail conversion logic.
docs/​configuration.md Explains overlap handling and positional-loss warnings.

Review effort: Balanced

yeongseon and others added 3 commits October 30, 2026 23:47
OpenAPI 3.0 requires a non-empty enum, so an empty enum next to a const (or a null-only type) is replaced by an allOf member of not: {}, which rejects every instance and is valid in 3.0.

The empty-prefix test no longer pins an items: true output, which OpenAPI 3.0 does not allow; that conversion is unchanged and belongs to a separate fix.

Refs #868

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
… 3.0

A tail schema beside prefixItems only constrains arrays that can hold more items than the prefix. With a maxItems at or below the prefix length it is unreachable, so appending it widened the positions the prefix does constrain (a string prefix with a number tail accepted [1]) and, being heterogeneous, was reported as a positional loss. The tail now joins the conversion only when maxItems is absent or exceeds the prefix length.

Refs #868

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
Only an enum met by a const or a null-only type is rewritten to not: {}; a standalone empty enum is not normalized.

Refs #868

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
@yeongseon
yeongseon merged commit c31009b into main Oct 30, 2026
33 checks passed
@yeongseon
yeongseon deleted the fix/spec-868-overlapping-keywords branch October 30, 2026 15:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(spec): keep the stricter constraint when 3.0 conversion folds overlapping keywords

2 participants