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

  1. 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" } } } } }).
  2. 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.
  3. 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

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


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)