Skip to content

[Bug]: OAS 3.1 dereferencer crashes on schemas with + , loses / during resolution #2331

@aqeelat

Description

@aqeelat

Description

The OAS 3.1 dereferencer (OpenAPIDereferencer31 + ReferenceVisitor) has two related issues that together prevent parsing any OpenAPI 3.1 spec that uses $dynamicRef / $dynamicAnchor:

  1. Hard crash: When a schema in components/schemas has both $id and $ref, the dereferencer resolves the $ref against the $id URI instead of the document root. For refs like #/components/schemas/..., this causes a RuntimeException: Could not find ....

  2. Data loss: $dynamicRef, $dynamicAnchor, and $defs are deserialized correctly but are not preserved through the resolution cycle. mergeSchemas() does not copy these fields, $defs is not traversed, and findAnchor() ignores $dynamicAnchor.

Environment

  • swagger-parser version: 2.1.41 (latest release)
  • OpenAPI version: 3.1.x

Reproduction

Case 1: $id + $ref crash

openapi: 3.1.0
info:
  title: Test
  version: 1.0.0
paths: {}
components:
  schemas:
    Template:
      type: object
      properties:
        items:
          type: array
    Concrete:
      $id: "https://example.com/schemas/Concrete"
      $ref: "#/components/schemas/Template"
ParseOptions options = new ParseOptions();
options.setResolve(true);
SwaggerParseResult result = new OpenAPIV3Parser().readLocation("spec.yaml", null, options);
// result.getMessages() contains:
// "Could not find /components/schemas/Template in contents of #/components/schemas/Template"

Case 2: $dynamicRef / $dynamicAnchor lost

openapi: 3.1.0
info:
  title: Test
  version: 1.0.0
paths: {}
components:
  schemas:
    Node:
      type: object
      properties:
        children:
          type: array
          items:
            $dynamicRef: "#node"
      $defs:
        node:
          $dynamicAnchor: node

After parsing with setResolve(true), $dynamicRef, $dynamicAnchor, and the $defs contents may not survive the dereference cycle.

Full reproduction specs

Minimal validator-backed OpenAPI 3.1 fixtures demonstrating both issues are available at:
https://github.com/aqeelat/openapi-dynamicref-adoption-tracker

Likely Cause Analysis

Bug 1: $id appears to hijack $ref resolution

In ReferenceVisitor.resolveSchemaRef() (line 230-279), the base URI is computed by chaining inheritedIds (the $id stack) against the current reference URI:

String baseURI = this.reference.getUri();
for (String id: inheritedIds) {
    baseURI = ReferenceUtils.resolve(ReferenceUtils.toBaseURI(id), baseURI);
    baseURI = ReferenceUtils.toBaseURI(baseURI);
}
baseURI = ReferenceUtils.resolve(ref, baseURI);

When Concrete has $id: "https://example.com/schemas/Concrete" and $ref: "#/components/schemas/Template":

  1. baseURI becomes https://example.com/schemas/Concrete
  2. resolve("#/components/schemas/Template", baseURI) yields https://example.com/schemas/Concrete#/components/schemas/Template
  3. The parser looks for JSON Pointer /components/schemas/Template inside the Concrete schema's JSON tree
  4. Concrete has no components property → crash

In pure JSON Schema, $id changing the base URI is correct. But in an OpenAPI document, #/components/schemas/... is a document-level reference that should resolve against the document root regardless of any schema's $id.

Bug 2: Data is not fully preserved during resolution

Field Deserialized? Preserved by mergeSchemas()? Traversed?
$dynamicRef Yes (getJsonSchema() line 4251) No N/A (not a container)
$dynamicAnchor Yes (getJsonSchema() line 4246) No N/A (not a container)
$defs Not mapped to a dedicated Schema field (retained as raw extension data) N/A No (traverseSchema() does not visit $defs)

Additionally, findAnchor() (line 281-308) only searches for $anchor, not $dynamicAnchor.

Expected Behavior

  1. #/components/schemas/... refs should always resolve against the document root, even when the enclosing schema has $id.
  2. $dynamicRef, $dynamicAnchor, and $defs should survive the parse + dereference cycle so that downstream tooling can access them.
  3. $defs sub-schemas should be traversed so that any $ref or $dynamicAnchor inside them is available in the parsed output.

Impact

This is reproducible in swagger-parser 2.1.41 with the fixtures above and affects OpenAPI 3.1 specs that use JSON Schema 2020-12 features for dynamic polymorphism — specifically the $dynamicRef / $dynamicAnchor pattern for recursive schemas, generic wrappers, and template types as described in:

Related downstream report (OpenAPI Generator, which depends on swagger-parser): OpenAPITools/openapi-generator#13087.

Proposed Fix Scope

This issue is scoped to parser correctness and data preservation only:

  1. avoid crashes for valid OAS 3.1 documents using $id + document-level $ref
  2. preserve $dynamicRef, $dynamicAnchor, and $defs through parse + dereference
  3. traverse $defs sub-schemas so nested references/anchors remain available

It does not request implementation of full JSON Schema 2020-12 dynamic scope resolution.

Related Issues


This issue was drafted with assistance from AI tooling. The submitter is responsible for reviewing and validating the contents before submission.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions