colinhacks/zod · error · Error

Duplicate schema id " " detected during JSON Schema…

Error message

Duplicate schema id "${id}" detected during JSON Schema conversion. Two different schemas cannot share the same id when converted together.

What it means

Thrown by extractDefs() while preparing $defs during JSON Schema conversion. The function walks ctx.seen and indexes every schema by its metadata id; if two distinct schema objects share the same id (existing && existing !== entry[0]), the conversion aborts because $defs keys must be unique. The id comes from ctx.metadataRegistry (default globalRegistry), typically set via .meta({ id: "..." }) or .register().

Solutions

  1. Give each schema a unique id in .meta({ id: ... }) or .register().
  2. If a duplicate is unintentional, find the second registration via a grep for the id and rename it.
  3. Use a namespaced id convention ("users.User", "orders.User") to avoid cross-module collisions.
  4. Clear or scope the global registry between unrelated conversions if leakage is the cause.

Example fix

// before
const A = z.object({ x: z.string() }).meta({ id: "thing" });
const B = z.object({ y: z.number() }).meta({ id: "thing" }); // duplicate
// after
const A = z.object({ x: z.string() }).meta({ id: "thingA" });
const B = z.object({ y: z.number() }).meta({ id: "thingB" });
Defensive patterns

Strategy: validation

Validate before calling

function assertUniqueIds(schemas: z.ZodType[], registry = z.globalRegistry) {
  const seen = new Map<string, z.ZodType>();
  for (const s of schemas) {
    const id = registry.get(s)?.id;
    if (!id) continue;
    if (seen.has(id) && seen.get(id) !== s) throw new Error(`Duplicate schema id: ${id}`);
    seen.set(id, s);
  }
}

Prevention

When it happens

Trigger: Registering two different schemas with the same id: z.schemaA.meta({ id: "user" }) and z.schemaB.meta({ id: "user" }), then converting a parent schema that references both; using a shared id constant across modules; copy-pasting a schema and its meta() without changing the id; two versions of a schema loaded via different import paths.

Common situations: Monorepo modules each registering their own "User" schema with id "user"; refactoring that splits one schema into two but keeps the id; dynamic schema factories that reuse a hardcoded id; test fixtures clashing with production registrations in the global registry.

Related errors


AI-assisted analysis of colinhacks/zod@2d90846af9 (2026-08-11). Data as JSON: /api/errors/da72081f692c3ed2. Report an issue: GitHub.

Appendix: source

Thrown at packages/zod/src/v4/core/to-json-schema.ts:235

export function extractDefs<T extends schemas.$ZodType>(
  ctx: ToJSONSchemaContext,
  schema: T
  // params: EmitParams
): void {
  // iterate over seen map;
  const root = ctx.seen.get(schema);

  if (!root) throw new Error("Unprocessed schema. This is a bug in Zod.");

  // Track ids to detect duplicates across different schemas
  const idToSchema = new Map<string, schemas.$ZodType>();
  for (const entry of ctx.seen.entries()) {
    const id = ctx.metadataRegistry.get(entry[0])?.id;
    if (id) {
      const existing = idToSchema.get(id);
      if (existing && existing !== entry[0]) {
        throw new Error(
          `Duplicate schema id "${id}" detected during JSON Schema conversion. Two different schemas cannot share the same id when converted together.`
        );
      }
      idToSchema.set(id, entry[0]);
    }
  }

  // returns a ref to the schema
  // defId will be empty if the ref points to an external schema (or #)
  const makeURI = (entry: [schemas.$ZodType<unknown, unknown>, Seen]): { ref: string; defId?: string } => {
    // comparing the seen objects because sometimes
    // multiple schemas map to the same seen object.
    // e.g. lazy

    // external is configured
    const defsSegment = ctx.target === "draft-2020-12" ? "$defs" : "definitions";
    if (ctx.external) {
      const externalId = ctx.external.registry.get(entry[0])?.id; // ?? "__shared";// `__schema${ctx.counter++}`;

View on GitHub (pinned to 2d90846af9)