Skip to content

SchemaRepresentation.toCodeDocument: no order of the definitions loads when recursive and non-recursive definitions use each other #10535

Open

Description

@fubhy

What happens

SchemaRepresentation.toCodeDocument returns the definitions in two groups: references.nonRecursives, topologically sorted, and references.recursives. When a recursive definition uses a non-recursive one, and a non-recursive definition uses a recursive one, neither order of the two groups gives a module that loads:

import { Schema, SchemaRepresentation } from "effect"

const Meta = Schema.Struct({ n: Schema.Number }).annotate({ identifier: "Meta" })

interface Tree {
  readonly meta: typeof Meta.Type
  readonly children: ReadonlyArray<Tree>
}
const Tree = Schema.Struct({
  meta: Meta,
  children: Schema.Array(Schema.suspend((): Schema.Codec<Tree> => Tree))
}).annotate({ identifier: "Tree" })

const Forest = Schema.Struct({ trees: Schema.Array(Tree) }).annotate({ identifier: "Forest" })

const { references } = SchemaRepresentation.toCodeDocument(
  SchemaRepresentation.toRepresentations([Forest.ast])
)

references holds:

// nonRecursives
const Meta = Schema.Struct({ "n": Schema.Number }).annotate({ "identifier": "Meta" })
const Forest = Schema.Struct({ "trees": Schema.Array(Tree) }).annotate({ "identifier": "Forest" })
// recursives
const Tree = Schema.Struct({ "meta": Meta, "children": Schema.Array(Schema.suspend((): Schema.Codec<Tree> => Tree)) }).annotate({ "identifier": "Tree" })

Writing these as a module and importing it:

  • nonRecursives, then recursives: ReferenceError: Cannot access 'Tree' before initialization, because Forest uses Tree directly.
  • recursives, then nonRecursives: ReferenceError: Cannot access 'Meta' before initialization, because Tree uses Meta directly.

An order that works exists (Meta, Tree, Forest), but CodeDocument doesn't say which definitions each one uses, so a consumer can only find it by scanning the generated strings. The OpenAPI generator does that today: collectForwardReferencedRecursives in packages/tools/openapi-generator/src/JsonSchemaGenerator.ts matches identifiers in code.runtime with a regex and adds a Schema.suspend alias for each recursive definition used before its declaration.

Expected

Printing the definitions in an order that CodeDocument gives (or documents) produces a module that loads, with no scanning of generated code.

Possible fixes, any of which would do:

  • Inside a non-recursive definition, print a reference to a recursive definition as Schema.suspend(…), as is already done inside recursive definitions. Then nonRecursives followed by recursives always loads.
  • Return the definitions in one list, sorted so that every direct reference points to an earlier entry.
  • Expose the definitions each definition refers to directly, so consumers can sort them.

Cause

In packages/effect/src/internal/schema/toCodeDocument.ts, recur prints a reference to a recursive definition as Schema.suspend(…) only while compilingRecursiveDefinition is set (line 382). Everywhere else, including inside non-recursive definitions, it prints the bare identifier. topologicalSort (line 112) orders only the non-recursive definitions, and nothing relates them to the recursive group.

Seen on main at b8c1622 (effect 4.1.3).

Activity

  1. self-assigned this
    on Nov 10, 2026
  2. assigned and unassigned on Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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