{"record":{"id":"96ac7ae76834e166","repo":"colinhacks/zod","slug":"cycle-detected-seen-cycle-join-root","errorCode":null,"errorMessage":"Cycle detected: #/${seen.cycle?.join(\"/\")}/<root>\n\nSet the `cycles` parameter to `\"ref\"` to resolve cyclical schemas with defs.","messagePattern":"Cycle detected: #/(.+?)/<root>\n\nSet the `cycles` parameter to `\"ref\"` to resolve cyclical schemas with defs\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/zod/src/v4/core/to-json-schema.ts","lineNumber":307,"sourceCode":"    // defId won't be set if the schema is a reference to an external schema\n    // or if the schema is the root schema\n    if (defId) seen.defId = defId;\n    // wipe away all properties except $ref\n    const schema = seen.schema;\n    for (const key in schema) {\n      delete schema[key];\n    }\n    schema.$ref = ref;\n  };\n\n  // throw on cycles\n\n  // break cycles\n  if (ctx.cycles === \"throw\") {\n    for (const entry of ctx.seen.entries()) {\n      const seen = entry[1];\n      if (seen.cycle) {\n        throw new Error(\n          \"Cycle detected: \" +\n            `#/${seen.cycle?.join(\"/\")}/<root>` +\n            '\\n\\nSet the `cycles` parameter to `\"ref\"` to resolve cyclical schemas with defs.'\n        );\n      }\n    }\n  }\n\n  // extract schemas into $defs\n  for (const entry of ctx.seen.entries()) {\n    const seen = entry[1];\n\n    // convert root schema to # $ref\n    if (schema === entry[0]) {\n      extractToDef(entry); // this has special handling for the root schema\n      continue;\n    }\n","sourceCodeStart":289,"sourceCodeEnd":325,"githubUrl":"https://github.com/colinhacks/zod/blob/2d90846af918af9602e088812d63a035d47cdbe4/packages/zod/src/v4/core/to-json-schema.ts#L289-L325","documentation":"Thrown by extractDefs() when ctx.cycles === \"throw\" (the default) and at least one schema in ctx.seen has a non-empty cycle path. Recursive schemas (e.g. a tree node that references itself) cannot be serialized to a flat JSON Schema without refs; the default is to refuse rather than emit infinitely-deep output. The message shows the cycle path (#/.../<root>) and tells the user to set cycles: \"ref\" to extract the recursion into $defs with $ref pointers.","triggerScenarios":"Calling z.toJSONSchema() on a recursive schema — typically one built with z.lazy() that references itself (e.g. a Tree type) — without setting the cycles option. Any self-reference, direct or transitive, produces a cycle in ctx.seen and triggers the throw under the default \"throw\" policy.","commonSituations":"Tree/list/graph domain models defined with z.lazy(); category-theoretic recursive types; converting a schema that references a registered sub-schema which in turn references the parent; first attempt at JSON Schema export before learning about the cycles option.","solutions":["Pass { cycles: \"ref\" } to z.toJSONSchema() so recursive schemas are emitted using $defs and $ref.","If refs are undesirable, refactor the schema to remove the cycle (e.g. bound depth, separate leaf type).","Register the recursive sub-schema with an id so the emitted $ref is stable and named.","For OpenAPI/Swagger consumers, confirm the target draft supports $ref/$defs (draft-2020-12 does)."],"exampleFix":"// before\nconst Tree = z.lazy(() => z.object({ value: z.number(), children: z.array(Tree) }));\nconst json = z.toJSONSchema(Tree); // throws: cycle\n// after\nconst json = z.toJSONSchema(Tree, { cycles: \"ref\" });","handlingStrategy":"fallback","validationCode":"function toJSONSchemaSafe(schema: z.ZodType) {\n  try {\n    return z.toJSONSchema(schema);\n  } catch (e) {\n    if (/Cycle detected/.test((e as Error).message)) {\n      return z.toJSONSchema(schema, { cycles: \"ref\" });\n    }\n    throw e;\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  return z.toJSONSchema(schema);\n} catch (e) {\n  if (/Cycle detected/.test((e as Error).message)) {\n    return z.toJSONSchema(schema, { cycles: \"ref\" });\n  }\n  throw e;\n}","preventionTips":["For any recursive (z.lazy) schema, always pass { cycles: \"ref\" }.","Register recursive sub-schemas with an id for stable $ref names.","Confirm the target JSON Schema draft supports $defs/$ref."],"tags":["json-schema","conversion","cycle","recursive","lazy","ref"],"backgroundTag":null,"analyzedSha":"2d90846af918af9602e088812d63a035d47cdbe4","analyzedAt":"2026-08-11T01:21:44.015Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}