{"id":"b8dca60b88fe8d23","repo":"colinhacks/zod","slug":"fromjsonschema-input-is-not-valid-json-possibly-c","errorCode":null,"errorMessage":"fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas","messagePattern":"fromJSONSchema input is not valid JSON \\(possibly cyclic\\); use \\$defs/\\$ref for recursive schemas","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/zod/src/v4/classic/from-json-schema.ts","lineNumber":643,"sourceCode":"}\n\n/**\n * Converts a JSON Schema to a Zod schema. This function should be considered semi-experimental. It's behavior is liable to change. */\nexport function fromJSONSchema(schema: JSONSchema.JSONSchema | boolean, params?: FromJSONSchemaParams): ZodType {\n  // Handle boolean schemas\n  if (typeof schema === \"boolean\") {\n    return schema ? z.any() : z.never();\n  }\n\n  // Normalize input via a JSON round-trip. This guarantees the converter\n  // walks a plain, finite, JSON-valid object graph: cyclic inputs fail here,\n  // getter/Proxy-based properties are materialized into static values, and\n  // class instances collapse to plain objects.\n  let normalized: JSONSchema.JSONSchema;\n  try {\n    normalized = JSON.parse(JSON.stringify(schema));\n  } catch {\n    throw new Error(\"fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas\");\n  }\n\n  const version = detectVersion(normalized, params?.defaultTarget);\n  const defs = (normalized.$defs || normalized.definitions || {}) as Record<string, JSONSchema.JSONSchema>;\n\n  const ctx: ConversionContext = {\n    version,\n    defs,\n    refs: new Map(),\n    processing: new Set(),\n    rootSchema: normalized,\n    registry: params?.registry ?? globalRegistry,\n  };\n\n  return convertSchema(normalized, ctx);\n}\n","sourceCodeStart":625,"sourceCodeEnd":660,"githubUrl":"https://github.com/colinhacks/zod/blob/912f0f51b0ced654d0069741e7160834dca742ee/packages/zod/src/v4/classic/from-json-schema.ts#L625-L660","documentation":"Thrown when `JSON.parse(JSON.stringify(schema))` fails inside fromJSONSchema. The round-trip normalises the input into a plain finite object graph; cyclic object graphs (and other non-JSON-serialisable inputs like BigInt, functions, or symbols) fail here. The error message explicitly points to `$defs/$ref` as the supported way to express recursion.","triggerScenarios":"Passing a JavaScript object that contains a real cycle (e.g. `a.self = a`) instead of representing recursion declaratively via `$defs` and `$ref`. Also triggered by non-JSON values such as BigInt keys, functions, or Proxies that throw during serialisation.","commonSituations":"Building the input schema programmatically and linking nodes by reference; converting class instances with circular back-pointers; deserialised JSON patched in memory to add cycles.","solutions":["Express recursion declaratively: hoist the recursive shape into `$defs` and reference it with `#/$defs/Name`.","Remove runtime cycles from the input object before passing it in (deep clone with a cycle-breaking library if needed).","Strip non-JSON values (BigInt, functions, symbols) before calling fromJSONSchema."],"exampleFix":"// before (runtime cycle)\nconst node = { type: \"object\", properties: {} };\nnode.properties.self = node; // cycle\nz4.fromJSONSchema(node); // throws\n\n// after (declarative recursion via $defs)\nconst schema = {\n  $defs: {\n    Node: {\n      type: \"object\",\n      properties: { self: { $ref: \"#/$defs/Node\" } }\n    }\n  },\n  $ref: \"#/$defs/Node\"\n};\nz4.fromJSONSchema(schema);","handlingStrategy":"validation","validationCode":"function assertJsonable(schema: unknown) {\n  JSON.stringify(schema); // throws on cycles / non-JSON values\n}","typeGuard":"function isPlainJson(v: any, seen = new WeakSet()): boolean {\n  if (v === null || typeof v !== \"object\") return [\"string\",\"number\",\"boolean\"].includes(typeof v) || v == null;\n  if (seen.has(v)) return false;\n  seen.add(v);\n  return Array.isArray(v) ? v.every((x) => isPlainJson(x, seen)) : Object.values(v).every((x) => isPlainJson(x, seen));\n}","tryCatchPattern":"try { const s = z4.fromJSONSchema(schema); }\ncatch (e) {\n  if (e instanceof Error && /not valid JSON|cyclic/.test(e.message)) {\n    // hoist recursive shape into $defs/$ref and retry\n  }\n  throw e;\n}","preventionTips":["Never build schema graphs with runtime cycles; use `$defs` + `$ref` for recursion.","Strip non-JSON values (BigInt, functions, symbols) before calling fromJSONSchema.","Validate input with JSON.stringify in tests to surface cycles early."],"tags":["json-schema","circular","serialization","v4","from-json-schema"],"analyzedSha":"912f0f51b0ced654d0069741e7160834dca742ee","analyzedAt":"2026-08-03T17:41:55.908Z","schemaVersion":2}