Skip to content

fix(spec): report JSON Schema keywords that OpenAPI 3.0 does not define #867

Closed

Description

@yeongseon

Context

OpenAPI 3.0 has no propertyNames, if/then/else, contains, unevaluatedProperties, dependentRequired, or array-valued items. When a schema carries one of them, 3.0 generation leaves it in the document. The document then fails openapi-spec-validator, no warning is emitted, and strict=True passes.

This is not limited to hand-written schemas. Pydantic v2 emits propertyNames for a dict field whose key type has constraints, so an ordinary model such as dict[Literal["a", "b"], int] produces an invalid 3.0 document.

The 3.0 residual audit only looks for type arrays and null anyOf branches, and conversion does not touch these keywords, so nothing in the pipeline notices them.

Current State

Measured with the fix from #863 applied (the state main will be in once it merges), openapi-spec-validator 0.9.0, Pydantic 2.14.2, Python 3.11, openapi_version="3.0.0" and strict=True:

Input Generation Structured warnings External validation
dict[Literal["a", "b"], int] field succeeds 0 fails
dict[Annotated[str, StringConstraints(max_length=3)], int] field succeeds 0 fails
dict[Annotated[str, StringConstraints(pattern="^item-")], int] field succeeds 0 valid (patternProperties is already converted)
raw {"if": ..., "then": ..., "else": ...} succeeds 0 fails
raw {"type": "array", "contains": {...}} succeeds 0 fails
raw {"type": "object", "unevaluatedProperties": false} succeeds 0 fails
raw {"type": "object", "dependentRequired": {...}} succeeds 0 fails
raw {"type": "array", "items": [{"type": "string"}]} succeeds 0 fails

For dict[Literal["a", "b"], int] Pydantic emits {"additionalProperties": {"type": "integer"}, "propertyNames": {"enum": ["a", "b"]}, "type": "object"}.

Steps to Reproduce

from typing import Literal

from openapi_spec_validator import validate
from pydantic import BaseModel

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


class Config(BaseModel):
    flags: dict[Literal["a", "b"], int]


clear_openapi_registry()


@openapi(route="config", method="post", requests=Config, summary="Set config")
def set_config() -> None: ...


report = generate_openapi_report(openapi_version="3.0.0", strict=True)
print(report.warnings)  # ()
validate(report.spec)  # raises OpenAPIValidationError

Acceptance Checklist

  • Decide, per keyword, whether 3.0 generation drops the keyword and reports the loss, or leaves it in place and reports it. Record the decision here. Dropping propertyNames and reporting it matches how fix(spec): reject or report lossy OpenAPI 3.0 schema conversion #782 treats heterogeneous tuples.
  • With openapi_version="3.0.0", the Pydantic models in the table above produce documents that pass openapi-spec-validator.
  • Every dropped or reported keyword produces a structured version-downgrade-drop warning that names the keyword and its location, and raises under strict=True.
  • Every keyword in the table is handled at every position conversion already visits (properties, items, additionalProperties, allOf/anyOf/oneOf, not, components and inline operation schemas).
  • Regression tests validate the generated 3.0 document with openapi-spec-validator for components and for inline positions.
  • docs/configuration.md and DESIGN.md no longer say that keywords OpenAPI 3.0 does not define are not audited.
  • make check-all passes and coverage stays at or above 95%.

Out of scope

  • OpenAPI 3.1 and 3.2 output.
  • Down-converting overlapping keywords that lose a constraint (tracked separately).
  • Non-schema OpenAPI 3.1 features such as webhooks or jsonSchemaDialect.

Environment

References

Metadata

Metadata

Assignees

No one assigned

    Labels

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions