{"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":179,"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":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/colinhacks/zod/blob/912f0f51b0ced654d0069741e7160834dca742ee/packages/zod/src/v4/classic/from-json-schema.ts#L161-L197","documentation":"Thrown inside the z.lazy() thunk emitted for a circular `$ref`. The converter defers recursive refs by returning a lazy schema that reads `ctx.refs.get(refPath)` on demand; if, at evaluation time, the ref was never populated (e.g. the cycle could not be resolved through `$defs`), the thunk throws.","triggerScenarios":"A self-referential or mutually recursive `$ref` whose target is not present in `$defs`/`definitions` (so resolveRef already failed), or a ref graph the converter's single-pass processing set could not close. Typically surfaces only when the returned Zod schema is actually used to parse data that exercises the recursion.","commonSituations":"Tree/node schemas with `#/$defs/Node` referencing itself; recursive OpenAPI components where the path is wrong; converting partial schemas whose `$defs` were stripped.","solutions":["Ensure the recursive target exists under `$defs`/`definitions` with the exact path used by the `$ref`.","Verify the `$ref` fragment is in `#/$defs/<Name>` form (or `#/definitions/<Name>` for draft-7).","Test conversion with sample data that triggers the recursion early to surface the failure during development."],"exampleFix":"// before\n{ \"$ref\": \"#/$defs/Node\" } // $defs.Node missing or mis-named\n\n// after\n{\n  \"$defs\": {\n    \"Node\": {\n      \"type\": \"object\",\n      \"properties\": { \"children\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/Node\" } } }\n    }\n  },\n  \"$ref\": \"#/$defs/Node\"\n}","handlingStrategy":"validation","validationCode":"function assertRecursiveRefsResolve(schema: any) {\n  const defs = schema.$defs ?? schema.definitions ?? {};\n  const visit = (n: any, seen = new Set<string>()): boolean => {\n    if (!n || typeof n !== \"object\") return true;\n    if (typeof n.$ref === \"string\") {\n      const key = n.$ref.split(\"/\").pop();\n      if (seen.has(n.$ref)) return key in defs; // cycle: target must exist\n      if (!defs[key]) return false;\n      return visit(defs[key], new Set(seen).add(n.$ref));\n    }\n    return Object.values(n).every((v) => visit(v, new Set(seen)));\n  };\n  if (!visit(schema)) throw new Error(\"Recursive $ref target missing\");\n}","typeGuard":"function refTargetExists(schema: any, ref: string): boolean {\n  const defs = schema.$defs ?? schema.definitions ?? {};\n  return ref.split(\"/\").pop()! in defs;\n}","tryCatchPattern":null,"preventionTips":["Test converted recursive schemas with sample data that exercises the recursion before production use.","Keep `$defs` self-contained and verify every recursive target exists.","Use `#/$defs/Name` consistently for self-reference."],"tags":["json-schema","circular","recursive","lazy","v4","from-json-schema"],"analyzedSha":"912f0f51b0ced654d0069741e7160834dca742ee","analyzedAt":"2026-08-03T17:41:55.908Z","schemaVersion":2}