colinhacks/zod · error · Error

fromJSONSchema input is not valid JSON (possibly cyclic); us

Error message

fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas

What it means

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.

Source

Thrown at packages/zod/src/v4/classic/from-json-schema.ts:643

}

/**
 * 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 912f0f51b0)

Solutions

  1. Express recursion declaratively: hoist the recursive shape into `$defs` and reference it with `#/$defs/Name`.
  2. Remove runtime cycles from the input object before passing it in (deep clone with a cycle-breaking library if needed).
  3. Strip non-JSON values (BigInt, functions, symbols) before calling fromJSONSchema.

Example fix

// before (runtime cycle)
const node = { type: "object", properties: {} };
node.properties.self = node; // cycle
z4.fromJSONSchema(node); // throws

// after (declarative recursion via $defs)
const schema = {
  $defs: {
    Node: {
      type: "object",
      properties: { self: { $ref: "#/$defs/Node" } }
    }
  },
  $ref: "#/$defs/Node"
};
z4.fromJSONSchema(schema);
Defensive patterns

Strategy: validation

Validate before calling

function assertJsonable(schema: unknown) {
  JSON.stringify(schema); // throws on cycles / non-JSON values
}

Type guard

function isPlainJson(v: any, seen = new WeakSet()): boolean {
  if (v === null || typeof v !== "object") return ["string","number","boolean"].includes(typeof v) || v == null;
  if (seen.has(v)) return false;
  seen.add(v);
  return Array.isArray(v) ? v.every((x) => isPlainJson(x, seen)) : Object.values(v).every((x) => isPlainJson(x, seen));
}

Try / catch

try { const s = z4.fromJSONSchema(schema); }
catch (e) {
  if (e instanceof Error && /not valid JSON|cyclic/.test(e.message)) {
    // hoist recursive shape into $defs/$ref and retry
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of colinhacks/zod@912f0f51b0 (2026-08-03). Data as JSON: /data/errors/b8dca60b88fe8d23.json. Report an issue: GitHub.