Skip to content

feat(schema): add first-class $defs field + traverse all sibling keywords in SpecFilter#5207

Open
aqeelat wants to merge 2 commits into
swagger-api:masterfrom
aqeelat:feat/issue-4469/add-defs-to-schema
Open

feat(schema): add first-class $defs field + traverse all sibling keywords in SpecFilter#5207
aqeelat wants to merge 2 commits into
swagger-api:masterfrom
aqeelat:feat/issue-4469/add-defs-to-schema

Conversation

@aqeelat

@aqeelat aqeelat commented Jun 17, 2026

Copy link
Copy Markdown

Description

Adds a first-class $defs field to io.swagger.v3.oas.models.media.Schema, mirroring how the other JSON Schema 2020-12 keywords ($dynamicAnchor, $dynamicRef, $anchor, $id, $schema, $vocabulary, patternProperties, dependentSchemas, …) were added. $defs is the canonical location for the generic-template binding used with $dynamicRef/$dynamicAnchor and was the notable omission. Also fixes two related gaps in SpecFilter.addSchemaRef that dropped schema references.

Fixes: #4469

What & why

1. First-class $defs field (feat(schema) commit)

  • Schema: $defs field + get$defs/set$defs/$defs(Map)/add$defs accessors, gated by @OpenAPI31; included in equals/hashCode/toString.
  • SchemaMixin: @JsonIgnore on get$defs() for V30 exclusion (matches the existing pattern for $id, $anchor, patternProperties, dependentSchemas, etc.).
  • V31 serializer/deserializer need no changes — auto-discovery via getters/setters, same as $dynamicAnchor/$dynamicRef.

Behavioral change for consumers: $defs in input JSON is now routed to the dedicated field rather than the extensions map. Producers using the extension-injection workaround continue to work (the $defs field stays null, the extensions entry is still emitted via @JsonAnyGetter). See the breaking-change analysis comment on the issue for migration impact.

2. SpecFilter.addSchemaRef restructure (fix(filter) commit)

While wiring $defs into the filter I uncovered two pre-existing bugs in addSchemaRef (used by RemoveUnreferencedDefinitionsFilter):

  1. Bug A — $ref siblings skipped. Seven keyword traversals sat after an early-return on $ref, so any sibling of $ref was skipped. With OAS 3.1 allowing $ref siblings, schemas like {$ref: Parent, $defs: {x: {$ref: Target}}} lost Target.
  2. Bug B — 9 JSON Schema 2020-12 keywords never traversed at all, $ref or not: not, prefixItems, contains, contentSchema, unevaluatedProperties, additionalItems, unevaluatedItems, if/else/then, dependentSchemas. A schema like {contains: {$ref: X}} dropped X.

Restructure:

  • Record $ref without early-return; always traverse siblings.
  • Traverse all 20 sibling keyword containers uniformly via traverseSchemaMap/traverseSchemaList helpers.
  • IdentityHashMap-backed visited-set for cycle protection.
  • Incidental fix: the old if/else chain made items and allOf/anyOf/oneOf mutually exclusive — an ArraySchema with allOf had its allOf entries silently dropped.

For OAS 3.0 documents the behavioral change is over-retention of referenced schemas (safe failure mode for a "remove unreferenced" filter); the worst case is a referenced schema surviving, never a needed one being dropped. Full sibling traversal was chosen over V31-gating for consistency with how every prior 2020-12 keyword (patternProperties, prefixItems, dependentSchemas, $dynamicAnchor, $dynamicRef) was wired up unconditionally, and because the missing-keyword traversals (Bug B) only affect keywords that cannot appear in strict OAS 3.0 docs anyway.

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • ♻️ Refactor (non-breaking change)
  • 🧪 Tests
  • 📝 Documentation
  • 🧹 Chore (build or tooling)

Checklist

  • I have added/updated tests as needed
  • I have added/updated documentation where applicable
  • The PR title is descriptive
  • The code builds and passes tests locally (mvn test on swagger-models + swagger-core; 69 SpecFilterTest cases pass, full swagger-core suite green)
  • I have linked related issues

Tests

  • OpenAPI3_1DeserializationTest: $defs round-trip, accumulation, equality, and the consumer-side deserialization routing change.
  • SwaggerSerializerTest: V31-only emission + extension-coexistence characterization (legacy workaround still works; mixing field + extensions produces duplicate keys, documented).
  • SpecFilterTest: 40 new data-driven cases via @DataProvider (20 sibling keywords × 2 scenarios — standalone {$ref: kw} and $ref sibling {$ref, kw}).

Downstream follow-ups (out of scope)

  • swagger-parser PR #2332 currently reads $defs from the extensions map; it should switch to get$defs() once this ships.
  • Micronaut OpenAPI has a SchemaDefinitionUtils.setSchemaDefs() helper behind a cached reflection check for set$defs — it auto-switches to the native method once available.

Additional Context

Detailed motivation, empirical evidence, and downstream layering context are in the issue thread (#4469). The two commits are split deliberately: reviewers who only care about the new field can focus on the first; the filter restructure is separable.

aqeelat added 2 commits June 17, 2026 18:47
Adds $defs to io.swagger.v3.oas.models.media.Schema, mirroring how the
other 2020-12 keywords ($dynamicAnchor, $dynamicRef, $anchor, $id,
$schema, $vocabulary, patternProperties, dependentSchemas, ...) were
added. $defs is the canonical location for the generic-template binding
used with $dynamicRef/$dynamicAnchor and was the notable omission.

- Schema: $defs field + get$defs/set$defs/$defs(Map)/add$defs
  accessors, gated by @OpenAPI31; included in equals/hashCode/toString
- SchemaMixin: @JsonIgnore on get$defs() for V30 exclusion
- V31 serializer/deserializer need no changes (auto-discovery via
  getters/setters, same as $dynamicAnchor etc.)

Behavioral change for consumers: $defs in input JSON is now routed to
the dedicated field rather than the extensions map. Producers using the
extension-injection workaround continue to work (the $defs field stays
null, extensions entry is still emitted via @JsonAnyGetter).

Refs: swagger-api#4469
addSchemaRef previously had two gaps that caused
RemoveUnreferencedDefinitionsFilter to silently drop schemas referenced
via certain keyword containers:

1. Bug A (OAS 3.1 $ref siblings): seven keyword traversals sat after
   an early return on $ref, so any sibling of $ref was skipped. With
   OAS 3.1 allowing $ref siblings, schemas like
   {$ref: Parent, $defs: {x: {$ref: Target}}} lost Target.

2. Bug B (missing traversals): nine JSON Schema 2020-12 keywords were
   never traversed at all, $ref or not: not, prefixItems, contains,
   contentSchema, unevaluatedProperties, additionalItems,
   unevaluatedItems, if/else/then, dependentSchemas. A schema like
   {contains: {$ref: X}} dropped X.

Restructure addSchemaRef to:
- record $ref without early-return; always traverse siblings
- traverse all 20 sibling keyword containers uniformly
- add an IdentityHashMap-backed visited-set for cycle protection
- drop the items/allOf mutual exclusivity (incidental fix: an
  ArraySchema with allOf previously had its allOf entries skipped)

For OAS 3.0 documents the behavioral change is over-retention of
referenced schemas (safe failure mode for a "remove unreferenced"
filter); the worst case is a referenced schema surviving, never a
needed one being dropped.

Tests: 40 new data-driven cases via \@dataProvider (20 keywords x
2 scenarios: standalone {$ref: kw} and $ref sibling {$ref, kw}).

Refs: swagger-api#4469
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

openapi 3.1.0 Schema is missing $defs information

1 participant