Skip to content

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

Closed

Description

@yeongseon

Context

When OpenAPI 3.0 conversion folds two overlapping JSON Schema keywords into one, it keeps the later one instead of the stricter one and reports nothing. The 3.0 document then accepts values the model rejects.

The most likely trigger is an ordinary Pydantic field: Field(ge=5, gt=1) emits both minimum: 5 and exclusiveMinimum: 1, and the converted schema keeps only minimum: 1 with exclusiveMinimum: true, so 2 and 3 are accepted. Two raw-schema cases behave the same way.

Current State

Measured with the fix from #863 applied (the conversion code involved is unchanged by it), openapi-schema-validator 0.9.0 (OAS31Validator on the original schema, OAS30Validator on the converted one), Python 3.11, Pydantic 2.14.2, openapi_version="3.0.0". Generation succeeds and no structured warning is emitted in any case.

Original schema Converted for 3.0 Accepted by 3.0 but rejected by the original
{"type": "integer", "minimum": 5, "exclusiveMinimum": 1} (Pydantic Field(ge=5, gt=1)) {"type": "integer", "minimum": 1, "exclusiveMinimum": true} 2, 3
{"enum": ["y"], "const": "x"} {"enum": ["x"]} "x"
{"type": "array", "prefixItems": [{"type": "string"}], "items": false} {"type": "array", "items": {"type": "string"}} ["a", "b"]

In the converter, the exclusive-bound rewrite overwrites the existing minimum (and maximum), const replaces any existing enum, and a single-position prefixItems is turned into items, overwriting an existing items value.

Steps to Reproduce

from openapi_schema_validator import OAS30Validator, OAS31Validator
from pydantic import BaseModel, Field

from azure_functions_openapi import generate_openapi_report, openapi
from azure_functions_openapi.decorator import clear_openapi_registry


class Both(BaseModel):
    n: int = Field(ge=5, gt=1)


clear_openapi_registry()


@openapi(route="both", method="post", requests=Both, summary="Both bounds")
def both() -> None: ...


report = generate_openapi_report(openapi_version="3.0.0", strict=True)
original = Both.model_json_schema()["properties"]["n"]
converted = report.spec["components"]["schemas"]["Both"]["properties"]["n"]
print(OAS31Validator(original).is_valid(2))   # False
print(OAS30Validator(converted).is_valid(2))  # True
print(report.warnings)                        # ()

Acceptance Checklist

  • When exclusiveMinimum or exclusiveMaximum collides with an existing minimum or maximum, the converted schema keeps the stricter constraint.
  • A schema that has both enum and const is converted without widening, or the loss is reported through the structured version-downgrade-drop channel and raises under strict=True.
  • A prefixItems list together with items: false keeps the position count (for example with minItems and maxItems), or the loss is reported the same way.
  • Regression tests compare the instances accepted by the original (OAS31Validator) and by the converted schema (OAS30Validator) for each case, inline and as a component.
  • make check-all passes and coverage stays at or above 95%.

Out of scope

  • Keywords that OpenAPI 3.0 does not define, such as propertyNames (tracked separately).
  • Behavior of nullable combined with an enum that contains null, which follows a documented heuristic (fix(spec): reject or report lossy OpenAPI 3.0 schema conversion #782).
  • Differences between the two validators in what counts as an integer (for example 1.0).

Environment

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingneeds-triageNot yet reviewed for validity, scope, type, and priority.priority: lowsize: S

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions