Repository navigation
fix(spec): keep the surrounding schema's keys when 3.0 conversion inlines a nullable reference #870
Description
Context
When OpenAPI 3.0 conversion collapses a nullable union whose only non-null member is a $ref (anyOf: [{"$ref": ...}, {"type": "null"}]), it inlines the referenced schema and merges the keys of the surrounding schema into it. On a conflict the referenced schema's value wins and the surrounding one is dropped without a warning, although the code comments say the field-level value wins. A realistic trigger is Optional[Model] = Field(default=None, description="..."): the 3.1 output shows the field description, the 3.0 output shows the model's docstring instead.
The automated review of #863 found related cases with allOf. They reproduce without any type array and on main.
Current State
Measured on main (be77a15) and with #863 applied, Python 3.11, Pydantic 2.14.2, openapi_version="3.0.0". Generation succeeds, no structured warning is emitted, and strict=True does not raise.
Field description (Pydantic model Account with home: Address | None = Field(default=None, description="Where the account holder lives"), where Address has the docstring """A postal address."""):
| OpenAPI version | components.schemas.Account.properties.home.description |
|---|---|
| 3.1 | Where the account holder lives |
| 3.0 | A postal address. |
Structural cases, measured with openapi-schema-validator 0.9.0 (OAS31Validator on the original schema, OAS30Validator on the converted one) and the referenced schema T = {"allOf": [{"type": "object"}]}:
| Original schema | Converted for 3.0 | Accepted by 3.0, rejected by the original | Rejected by 3.0, accepted by the original |
|---|---|---|---|
{"allOf": [{"minProperties": 1}], "anyOf": [{"$ref": T}, {"type": "null"}]} (also on main) |
{"allOf": [{"type": "object"}], "nullable": true} |
{} |
null |
{"type": ["string", "integer"], "anyOf": [{"$ref": T}, {"type": "null"}]} (needs #863) |
{"allOf": [{"type": "object"}], "nullable": true} |
{} |
none |
Both rows lose the surrounding allOf because the merge keeps the referenced schema's own allOf; the first row also rejects null because nullable never reaches the allOf members of the inlined schema.
Acceptance Checklist
- When a field-level annotation (
description,title,default, ...) conflicts with the referenced schema's, the converted 3.0 schema keeps the field-level value, as the 3.1 output does. - When the surrounding schema and the referenced schema both carry
allOf, the converted schema keeps both lists. - An inlined schema whose null acceptance depends on an
allOfmember acceptsnull, or the loss is reported through the structuredversion-downgrade-dropchannel and raises understrict=True. - Regression tests cover the Pydantic description case and compare the instances accepted by the original (
OAS31Validator) and by the converted schema (OAS30Validator) for the structural rows, inline and as a component. -
make check-allpasses and coverage stays at or above 95%.
Out of scope
- Folding of overlapping numeric,
enum/constandprefixItemskeywords (fix(spec): keep the stricter constraint when 3.0 conversion folds overlapping keywords #868). - Keywords that OpenAPI 3.0 does not define (fix(spec): report JSON Schema keywords that OpenAPI 3.0 does not define #867).
- Behavior of
nullablecombined with anenumthat containsnull, which follows a documented heuristic (fix(spec): reject or report lossy OpenAPI 3.0 schema conversion #782).
References
- fix(spec): convert non-null type arrays and audit inline schemas for OpenAPI 3.0 #863: the pull request whose automated review found the
allOfcases - fix(spec): reject or report lossy OpenAPI 3.0 schema conversion #782: nullable
$refinlining for OpenAPI 3.0 - fix(spec): downconvert nested container nullability to valid OpenAPI 3.0 form #562: down-conversion of nullable unions for OpenAPI 3.0
Activity
fix(spec): preserve nullable union predicates during downgrade
docs(spec): clarify discriminator scope and coupled bounds
test(spec): pin ref sibling fallback in multi-member unions
fix(spec): keep discriminators inert when a nullable union is written…
fix(spec): keep an inlined discriminator inert when merging field con…
fix(spec): keep every null member and its answer when a nullable unio…
fix(spec): do not take an unresolved reference as proof and keep obje…
fix(spec): resolve pointers into components and never restore an answ…