colinhacks/zod · error · Error
fromJSONSchema input is not valid JSON (possibly cyclic)…
Error message
fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas
What it means
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.
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.
Example fix
// before (cyclic — throws)
const node = { type: 'object', properties: { value: { type: 'string' } } };
node.properties.children = { type: 'array', items: node };
z.fromJSONSchema(node);
// after (acyclic via $ref)
const schema = {
$defs: {
Node: {
type: 'object',
properties: {
value: { type: 'string' },
children: { type: 'array', items: { $ref: '#/$defs/Node' } },
},
},
},
$ref: '#/$defs/Node',
};
z.fromJSONSchema(schema); Defensive patterns
Strategy: validation
Validate before calling
// Run the same round-trip the converter will run, before calling it.
function isJSONSchemaConvertible(value, seen = new WeakSet()) {
if (value === null || typeof value !== 'object') return true;
if (typeof value === 'function' || typeof value === 'symbol') return false;
if (seen.has(value)) return false; // cycle
seen.add(value);
return Object.values(value).every((v) => isJSONSchemaConvertible(v, seen));
}
if (!isJSONSchemaConvertible(mySchema)) {
throw new Error('Schema is cyclic or non-JSON; rewrite using $defs/$ref');
} Try / catch
try {
const zodSchema = z.fromJSONSchema(jsonSchema);
} catch (e) {
if (e.message.startsWith('fromJSONSchema input is not valid JSON')) {
// Surface the original TypeError for a more actionable stack
const probe = JSON.stringify(jsonSchema, null, 2);
throw new Error(`Input not convertible: ${e.message}`);
}
throw e;
} Prevention
- 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().
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Circular reference not resolved
- Conditional schemas (if/then/else) are not supported
- dependentSchemas and dependentRequired are not supported
- Error converting schema to JSON.
- External $ref is not supported, only local refs (#/...) are…
AI-assisted analysis of colinhacks/zod@2d90846af9 (2026-08-11).
Data as JSON: /api/errors/b8dca60b88fe8d23.
Report an issue: GitHub.
Appendix: source
Thrown at packages/zod/src/v4/classic/from-json-schema.ts:645
}
/**
* Converts a JSON Schema to a Zod schema. This function should be considered semi-experimental. It's behavior is liable to change. */
export function fromJSONSchema(schema: JSONSchema.JSONSchema | boolean, params?: FromJSONSchemaParams): ZodType {
// Handle boolean schemas
if (typeof schema === "boolean") {
return schema ? z.any() : z.never();
}
// Normalize input via a JSON round-trip. This guarantees the converter
// walks a plain, finite, JSON-valid object graph: cyclic inputs fail here,
// getter/Proxy-based properties are materialized into static values, and
// class instances collapse to plain objects.
let normalized: JSONSchema.JSONSchema;
try {
normalized = JSON.parse(JSON.stringify(schema));
} catch {
throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
}
const version = detectVersion(normalized, params?.defaultTarget);
const defs = (normalized.$defs || normalized.definitions || {}) as Record<string, JSONSchema.JSONSchema>;
const ctx: ConversionContext = {
version,
defs,
refs: new Map(),
processing: new Set(),
rootSchema: normalized,
registry: params?.registry ?? globalRegistry,
};
return convertSchema(normalized, ctx);
}
View on GitHub (pinned to 2d90846af9)