fix: strip discriminator after dereferencing schemas (#3682)

This commit is contained in:
Jeremiah Lowin 2026-03-28 19:46:05 -04:00 committed by GitHub
commit 923695bd9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 95 additions and 0 deletions

View file

@ -73,6 +73,33 @@ def _strip_remote_refs(obj: Any) -> Any:
return obj
def _strip_discriminator(obj: Any) -> Any:
"""Recursively remove OpenAPI ``discriminator`` keys from a schema.
Pydantic emits ``discriminator.mapping`` with values like
``#/$defs/ClassName``. After ``$defs`` are inlined and removed by
``dereference_refs``, those mapping entries dangle. The keyword is an
OpenAPI extension the ``anyOf`` variants already carry ``const`` on
the discriminant field, so the mapping is redundant.
Only strips ``discriminator`` when it appears alongside ``anyOf`` or
``oneOf``, which is where the OpenAPI keyword lives. A property
*named* ``discriminator`` (inside ``properties``) is left alone.
"""
if isinstance(obj, dict):
skip = "discriminator" in obj and ("anyOf" in obj or "oneOf" in obj)
# Keys that hold instance data, not sub-schemas — don't recurse.
_DATA_KEYS = {"default", "const", "examples", "enum"}
return {
k: (v if k in _DATA_KEYS else _strip_discriminator(v))
for k, v in obj.items()
if not (k == "discriminator" and skip)
}
if isinstance(obj, list):
return [_strip_discriminator(item) for item in obj]
return obj
def dereference_refs(schema: dict[str, Any]) -> dict[str, Any]:
"""Resolve all $ref references in a JSON schema by inlining definitions.
@ -135,6 +162,13 @@ def dereference_refs(schema: dict[str, Any]) -> dict[str, Any]:
if "$defs" in dereferenced:
dereferenced = {k: v for k, v in dereferenced.items() if k != "$defs"}
# Strip `discriminator` keys — they contain `mapping` values that
# point at `#/$defs/...` entries we just removed. `discriminator`
# is an OpenAPI extension; after inlining, the `anyOf` variants
# already carry `const` on the discriminant field, making the
# mapping redundant.
dereferenced = _strip_discriminator(dereferenced)
return dereferenced
except JsonRefError:

View file

@ -197,6 +197,67 @@ class TestDereferenceRefs:
assert country["default"] == "US"
assert "$defs" not in result
def test_strips_discriminator_mapping_after_inlining(self):
"""Discriminator.mapping refs dangle after $defs are inlined (#3679)."""
schema = {
"$defs": {
"IdentifyPerson": {
"type": "object",
"properties": {
"action": {"const": "identify", "type": "string"},
"name": {"type": "string"},
},
"required": ["action", "name"],
},
"PersonDelete": {
"type": "object",
"properties": {
"action": {"const": "delete", "type": "string"},
},
"required": ["action"],
},
},
"anyOf": [
{"$ref": "#/$defs/IdentifyPerson"},
{"$ref": "#/$defs/PersonDelete"},
],
"discriminator": {
"mapping": {
"identify": "#/$defs/IdentifyPerson",
"delete": "#/$defs/PersonDelete",
},
"propertyName": "action",
},
}
result = dereference_refs(schema)
assert "$defs" not in result
assert "discriminator" not in result
# The anyOf variants should be inlined with their const values intact
assert len(result["anyOf"]) == 2
actions = {v["properties"]["action"]["const"] for v in result["anyOf"]}
assert actions == {"identify", "delete"}
def test_preserves_property_named_discriminator(self):
"""A field *named* 'discriminator' inside properties must survive."""
schema = {
"$defs": {
"Inner": {
"type": "object",
"properties": {
"discriminator": {"type": "string"},
},
},
},
"properties": {
"item": {"$ref": "#/$defs/Inner"},
},
}
result = dereference_refs(schema)
assert "$defs" not in result
assert "discriminator" in result["properties"]["item"]["properties"]
class TestCompressSchema:
"""Tests for the compress_schema function."""