Skip to content

Recursive $ref in AsyncAPI input: extracted payload keeps an unresolvable document-absolute $ref #448

Description

@jonaslagoni

Split out of #447, which reported recursive $ref failures for both OpenAPI and AsyncAPI input. Those turned out to have two unrelated causes:

Summary

For AsyncAPI input, the payload the CLI extracts and hands to Modelina still contains a document-absolute $ref pointing at #/components/schemas/..., but the document it points into is not passed along. Modelina re-dereferences that fragment as a standalone JSON Schema, cannot resolve the pointer, and fails.

Version: 0.81.1, also reproduces on main (with @asyncapi/modelina 6.0.0-next.15).

Reproduction

asyncapi.yaml:

asyncapi: 3.0.0
info:
  title: Recursive
  version: 1.0.0
channels:
  nodes:
    address: nodes
    messages:
      Node:
        payload:
          $ref: '#/components/schemas/Node'
operations:
  sendNode:
    action: send
    channel:
      $ref: '#/channels/nodes'
components:
  schemas:
    Node:
      type: object
      required: [label]
      properties:
        label:
          type: string
        children:
          type: array
          items:
            $ref: '#/components/schemas/Node'

codegen.config.js:

export default {
  inputType: 'asyncapi',
  inputPath: './asyncapi.yaml',
  language: 'typescript',
  generators: [{preset: 'payloads', outputPath: './out'}]
};
$ codegen generate ./codegen.config.js
Generating code...
✗ Generation failed

Details:
Could not dereference $ref in input, is all the references correct? 1 error occurred while reading '<cwd>'

What is actually handed to Modelina

Dumping the input at the point Modelina dereferences it (JsonSchemaInputProcessor.dereferenceInputs):

{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema",
  "required": ["label"],
  "properties": {
    "label": {
      "type": "string",
      "x-parser-schema-id": "<anonymous-schema-1>",
      "x-modelgen-inferred-name": "NodeLabel"
    },
    "children": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/Node",
        "x-parser-schema-id": "<anonymous-schema-3>",
        "x-modelgen-inferred-name": "NodeChildrenItem"
      },
      "x-parser-schema-id": "<anonymous-schema-2>",
      "x-modelgen-inferred-name": "NodeChildren"
    }
  },
  "x-parser-schema-id": "Node",
  "x-modelgen-inferred-name": "root",
  "$id": "Node"
}

The underlying error from @apidevtools/json-schema-ref-parser is:

Missing $ref pointer "#/components/schemas/Node". Token "components" does not exist.

Why this happens

  1. src/codegen/inputs/asyncapi/parser.ts parses the document with @asyncapi/parser. Its resolver (@stoplight/json-ref-resolver) deliberately leaves circular $refs in place rather than materialising an infinite/cyclic structure. Non-recursive $refs get inlined; the self-reference does not.
  2. src/codegen/inputs/asyncapi/generators/payloads.ts extracts a single payload with AsyncAPIInputProcessor.convertToInternalSchema(message.payload()) and wraps it as a standalone root with $schema: 'http://json-schema.org/draft-07/schema'.
  3. src/codegen/output/modelina.ts passes that fragment to generator.generateCompleteModels(input, …).
  4. Modelina routes it to its JSON Schema processor, whose processDraft7 calls dereferenceInputs. That tries to resolve #/components/schemas/Node against the fragment — which has no components key — and throws.

The pointer's target was left behind with the document, so this is not resolvable from inside Modelina: the input it receives genuinely does not contain the referenced schema. Modelina's own AsyncAPIInputProcessor does not hit this because it never re-dereferences — it goes straight from convertToInternalSchema to convertSchemaToMetaModel.

Deleting the children property makes the document generate fine, confirming the recursion (and hence the unresolved $ref) is the trigger, not the surrounding document.

Possible directions

Roughly in order of how self-contained they look from the outside — a maintainer will know better which fits the architecture:

  • Rewrite a self-pointer to # when the extracted payload is the schema being pointed at, so the fragment becomes self-contained.
  • Carry the referenced schemas along, e.g. emit the payload with a definitions/$defs section containing the reachable components/schemas entries and rewrite pointers accordingly.
  • Resolve the cycle into a real JavaScript object cycle before handing it over, which is what the OpenAPI path already effectively does via @readme/openapi-parser. Note this requires fix: do not serialize input during processor detection asyncapi/modelina#2624 (merged, unreleased at time of writing) to avoid the Converting circular structure to JSON crash.

Notes

Recursive structures are common in real documents — category trees, comment threads, nested rule/filter expressions, JSON-Schema-shaped payloads.

For reference, the OpenAPI half of #447 now generates correctly once the Modelina fix is in place, producing the expected self-referencing model:

interface GetNodeResponse_200Interface {
  label: string
  children?: Node[]
}

The AsyncAPI case above is unaffected by that fix and still fails.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions