Repository navigation
fix(spec): keep the stricter constraint when 3.0 conversion folds overlapping keywords #868
Description
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
exclusiveMinimumorexclusiveMaximumcollides with an existingminimumormaximum, the converted schema keeps the stricter constraint. - A schema that has both
enumandconstis converted without widening, or the loss is reported through the structuredversion-downgrade-dropchannel and raises understrict=True. - A
prefixItemslist together withitems: falsekeeps the position count (for example withminItemsandmaxItems), 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-allpasses 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
nullablecombined with anenumthat containsnull, 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 example1.0).
Environment
azure-functions-openapi0.30.1 plus the change from fix(spec): convert non-null type arrays and audit inline schemas for OpenAPI 3.0 #863; Python 3.11; Pydantic 2.14.2;openapi-schema-validator0.9.0
References
- fix(spec): down-convert exclusive numeric bounds for OpenAPI 3.0 #694: exclusive bound down-conversion for OpenAPI 3.0
- fix(spec): reject or report lossy OpenAPI 3.0 schema conversion #782: reject or report lossy OpenAPI 3.0 schema conversion
- fix(spec): make OpenAPI 3.0 output valid for @validate_http bridge routes #858 and fix(spec): convert non-null type arrays and audit inline schemas for OpenAPI 3.0 #863: the 3.0 output fix during whose review these were found
Activity
fix(spec): emit a valid schema for an enum that accepts nothing in 3.0
fix(spec): ignore an unreachable prefixItems tail when converting for…
docs(spec): limit the empty-enum note to enum and const intersections