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
- Give each schema a unique id in .meta({ id: ... }) or .register().
- If a duplicate is unintentional, find the second registration via a grep for the id and rename it.
- Use a namespaced id convention ("users.User", "orders.User") to avoid cross-module collisions.
- 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
- Use unique, namespaced ids in .meta({ id }) and .register().
- Audit schema registrations when splitting modules.
- Clear the global registry in long-lived test processes if fixtures collide.
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
- Error converting schema to JSON.
- Schema is missing an `id` property
- Circular reference not resolved
- Conditional schemas (if/then/else) are not supported
- Cycle detected: #/ / Set the `cycles` parameter to `"ref"`…
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)