{"record":{"id":"2d21be4087dd5881","repo":"colinhacks/zod","slug":"circular-reference-not-resolved-refpath","errorCode":null,"errorMessage":"Circular reference not resolved: ${refPath}","messagePattern":"Circular reference not resolved: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/zod/src/v4/classic/from-json-schema.ts","lineNumber":180,"sourceCode":"  if (schema.if !== undefined || schema.then !== undefined || schema.else !== undefined) {\n    throw new Error(\"Conditional schemas (if/then/else) are not supported\");\n  }\n  if (schema.dependentSchemas !== undefined || schema.dependentRequired !== undefined) {\n    throw new Error(\"dependentSchemas and dependentRequired are not supported\");\n  }\n\n  // Handle $ref\n  if (schema.$ref) {\n    const refPath = schema.$ref;\n    if (ctx.refs.has(refPath)) {\n      return ctx.refs.get(refPath)!;\n    }\n\n    if (ctx.processing.has(refPath)) {\n      // Circular reference - use lazy\n      return z.lazy(() => {\n        if (!ctx.refs.has(refPath)) {\n          throw new Error(`Circular reference not resolved: ${refPath}`);\n        }\n        return ctx.refs.get(refPath)!;\n      });\n    }\n\n    ctx.processing.add(refPath);\n    const resolved = resolveRef(refPath, ctx);\n    const zodSchema = convertSchema(resolved, ctx);\n    ctx.refs.set(refPath, zodSchema);\n    ctx.processing.delete(refPath);\n    return zodSchema;\n  }\n\n  // Handle enum\n  if (schema.enum !== undefined) {\n    const enumValues = schema.enum;\n\n    // Special case: OpenAPI 3.0 null representation { type: \"string\", nullable: true, enum: [null] }","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/colinhacks/zod/blob/2d90846af918af9602e088812d63a035d47cdbe4/packages/zod/src/v4/classic/from-json-schema.ts#L162-L198","documentation":"Thrown lazily (packages/zod/src/v4/classic/from-json-schema.ts:180) at parse time — not conversion time — when a circular $ref was detected during conversion and wrapped in z.lazy(), but when the lazy actually evaluates the referenced schema was never registered in ctx.refs. The lazy closure expects a previous conversion pass to have populated the map; if the cycle broke before that happened (e.g. the ref's target itself failed to convert), the closure throws.","triggerScenarios":"Calling fromJSONSchema on a schema with a self-referential or mutually-recursive $ref (e.g. a tree node whose children reference the node itself), where the resolution of the referenced definition failed or the referenced path is invalid, so ctx.refs never receives the resolved ZodType. The throw surfaces only when the resulting schema is later used to parse data.","commonSituations":"Recursive schemas (linked lists, trees, graphs) where a nested $ref points at a definition that itself has an unsupported keyword, causing its conversion to fail mid-cycle; broken recursive references that look correct at conversion but reference a non-convertible subschema; cycles that pass through an unsupported construct.","solutions":["Inspect the refPath in the message — it names the $ref whose target never resolved; open that definition and check it converts cleanly in isolation.","Remove or rewrite any unsupported keywords on the referenced definition so its conversion completes and ctx.refs is populated.","Verify the recursive $ref actually points to a valid #/$defs/Name entry that can be converted standalone.","If the cycle is intentional but unsupported, hand-author the recursive Zod schema with z.lazy(() => schema) referencing a declared variable."],"exampleFix":"// before — recursive ref to a definition that fails to convert\nconst schema = {\n  $defs: {\n    Node: {\n      type: 'object',\n      properties: { children: { type: 'array', items: { $ref: '#/$defs/Node' } } },\n      if: { properties: { leaf: { const: true } } }, // unsupported keyword breaks conversion\n      then: {},\n    },\n  },\n  $ref: '#/$defs/Node',\n};\n\n// after — remove the unsupported keyword so the cycle resolves\nconst schema = {\n  $defs: {\n    Node: {\n      type: 'object',\n      properties: {\n        leaf: { type: 'boolean' },\n        children: { type: 'array', items: { $ref: '#/$defs/Node' } },\n      },\n    },\n  },\n  $ref: '#/$defs/Node',\n};","handlingStrategy":"try-catch","validationCode":"// Pre-validate that every recursive $ref's target converts standalone.\nfunction assertRecursiveRefsConvertible(root: any, version: 'draft-2020-12' | 'draft-7', fromJSONSchema: (s: any) => unknown) {\n  const defsKey = version === 'draft-2020-12' ? '$defs' : 'definitions';\n  const defs = root?.[defsKey] ?? {};\n  for (const [name, sub] of Object.entries(defs) as [string, any][]) {\n    try {\n      // convert the definition WITHOUT its siblings to surface failures\n      fromJSONSchema({ ...sub, $defs: defs });\n    } catch (e) {\n      throw new Error(`Definition '${name}' (target of a possible recursive ref) fails to convert: ${(e as Error).message}`);\n    }\n  }\n}","typeGuard":"null","tryCatchPattern":"// Wrap parse of a recursive schema so the lazy-eval failure is observable.\ntry {\n  result = zodSchema.parse(data);\n} catch (e) {\n  if (e instanceof Error && /Circular reference not resolved/.test(e.message)) {\n    // the refPath in the message names the unresolved target;\n    // inspect that definition for unsupported keywords.\n    console.error('Recursive conversion broke at:', e.message);\n  }\n  throw e;\n}","preventionTips":["Convert each #/$defs entry in isolation first to surface unsupported keywords on recursive targets.","Keep recursive definitions free of unsupported keywords (not, if/then/else, unevaluated*).","When the converter supports the cycle, prefer it; otherwise hand-author with z.lazy(() => nodeSchema).","Unit-test recursive schemas by parsing a small nested payload to trigger the lazy evaluation."],"tags":["json-schema","from-json-schema","conversion","circular-ref","recursive","lazy"],"backgroundTag":null,"analyzedSha":"2d90846af918af9602e088812d63a035d47cdbe4","analyzedAt":"2026-08-11T01:21:44.015Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}