{"record":{"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":645,"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":627,"sourceCodeEnd":662,"githubUrl":"https://github.com/colinhacks/zod/blob/2d90846af918af9602e088812d63a035d47cdbe4/packages/zod/src/v4/classic/from-json-schema.ts#L627-L662","documentation":"Thrown by fromJSONSchema() when its input cannot survive a JSON.parse(JSON.stringify(schema)) round-trip. The round-trip is used to normalize the input into a plain, finite object graph; cyclic references and non-JSON values (functions, symbols, undefined) cause JSON.stringify to throw, which is caught and re-thrown as this error. The message directs you to JSON Schema's $defs/$ref mechanism, which is the supported way to express recursive schemas.","triggerScenarios":"Calling fromJSONSchema() with an object that contains a cycle (e.g. a node whose property points back to a parent node), a class instance with getters that return the instance, a Proxy, or any value carrying functions/symbols/undefined. A self-referential TypeScript interface serialized naively (without $ref) produces this.","commonSituations":"Converting a hand-built or library-generated JSON Schema that models a tree/linked-list/graph with direct object nesting instead of $ref. Passing a live Zod schema or Mongoose schema object into fromJSONSchema by mistake. Loading a schema from a structured-clone boundary that preserved cycles.","solutions":["Rewrite the recursive portion of the schema to use $defs + $ref pointers so the object graph is acyclic (e.g. { \"$defs\": { \"Node\": { ... \"children\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/Node\" } } } } }).","If the input legitimately has no cycle, strip non-JSON values (functions, symbols, undefined) before calling fromJSONSchema, or run JSON.parse(JSON.stringify(schema)) yourself first to surface the real TypeError.","If you actually hold a Zod schema, do not pass it to fromJSONSchema — that function consumes JSON Schema, not Zod."],"exampleFix":"// before (cyclic — throws)\nconst node = { type: 'object', properties: { value: { type: 'string' } } };\nnode.properties.children = { type: 'array', items: node };\nz.fromJSONSchema(node);\n\n// after (acyclic via $ref)\nconst schema = {\n  $defs: {\n    Node: {\n      type: 'object',\n      properties: {\n        value: { type: 'string' },\n        children: { type: 'array', items: { $ref: '#/$defs/Node' } },\n      },\n    },\n  },\n  $ref: '#/$defs/Node',\n};\nz.fromJSONSchema(schema);","handlingStrategy":"validation","validationCode":"// Run the same round-trip the converter will run, before calling it.\nfunction isJSONSchemaConvertible(value, seen = new WeakSet()) {\n  if (value === null || typeof value !== 'object') return true;\n  if (typeof value === 'function' || typeof value === 'symbol') return false;\n  if (seen.has(value)) return false; // cycle\n  seen.add(value);\n  return Object.values(value).every((v) => isJSONSchemaConvertible(v, seen));\n}\n\nif (!isJSONSchemaConvertible(mySchema)) {\n  throw new Error('Schema is cyclic or non-JSON; rewrite using $defs/$ref');\n}","typeGuard":null,"tryCatchPattern":"try {\n  const zodSchema = z.fromJSONSchema(jsonSchema);\n} catch (e) {\n  if (e.message.startsWith('fromJSONSchema input is not valid JSON')) {\n    // Surface the original TypeError for a more actionable stack\n    const probe = JSON.stringify(jsonSchema, null, 2);\n    throw new Error(`Input not convertible: ${e.message}`);\n  }\n  throw e;\n}","preventionTips":["Author recursive JSON Schemas with $defs/$ref from the start; never build cycles by assigning object properties.","Validate external/schema-registry input through z.fromJSONSchema() only after a JSON.parse(JSON.stringify()) smoke test in development.","Keep a fixture test that round-trips every schema you feed to fromJSONSchema()."],"tags":["json-schema","from-json-schema","recursion","serialization"],"backgroundTag":null,"analyzedSha":"2d90846af918af9602e088812d63a035d47cdbe4","analyzedAt":"2026-08-11T01:21:44.015Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}