colinhacks/zod · error · Error
Schema is missing an `id` property
Error message
Schema is missing an `id` property
What it means
Thrown during JSON Schema finalization when ctx.external?.uri is set (the conversion is using an external $defs registry) but the root schema has no id in the external registry. The emitter needs an id to build the external URI (result.$id = ctx.external.uri(id)); without one it cannot produce a valid reference and aborts. The id is looked up via ctx.external.registry.get(schema)?.id.
Solutions
- Register the root schema in the external registry with an id before conversion: registry.add(schema, { id: "mySchema" }).
- Ensure ctx.external.registry is the same registry the schema was registered against.
- If you do not need external URIs, omit the external option entirely.
- Verify the id is set on metadata, not just on the schema instance, in the registry used by external.
Example fix
// before
const reg = z.registry();
const json = z.toJSONSchema(MySchema, { external: { registry: reg, uri: (id) => `/${id}` } });
// after
const reg = z.registry();
reg.add(MySchema, { id: "mySchema" });
const json = z.toJSONSchema(MySchema, { external: { registry: reg, uri: (id) => `/${id}` } }); Defensive patterns
Strategy: validation
Validate before calling
function toJSONSchemaExternal(schema: z.ZodType, registry: z.ZodRegistry) {
const id = registry.get(schema)?.id;
if (!id) throw new Error("Schema must be registered with an id before external conversion.");
return z.toJSONSchema(schema, { external: { registry, uri: (i) => `/${i}` } });
} Type guard
function hasRegistryId(schema: z.ZodType, registry: z.ZodRegistry): boolean {
return !!registry.get(schema)?.id;
} Prevention
- Register every root schema in the external registry with an id before conversion.
- Use the same registry instance for registration and for the external option.
- Skip the external option entirely if you don't need cross-file $ref URIs.
When it happens
Trigger: Calling z.toJSONSchema(schema, { external: { registry, uri } }) where `schema` was not registered in `registry` with an id (or at all). Forgetting the .register() call or omitting the id from the metadata when wiring up the external registry triggers it at conversion time.
Common situations: Multi-file JSON Schema setups that share a registry; refactoring that moves a schema out of the registry; renaming the registry without re-registering; assuming the global registry's id carries over to a custom external registry.
Related errors
- Duplicate schema id " " detected during JSON Schema…
- External $ref is not supported, only local refs (#/...) are…
- 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/e5daad3e436f0113.
Report an issue: GitHub.
Appendix: source
Thrown at packages/zod/src/v4/core/to-json-schema.ts:469
flattenRef(entry[0]);
}
const result: JSONSchema.BaseSchema = {};
if (ctx.target === "draft-2020-12") {
result.$schema = "https://json-schema.org/draft/2020-12/schema";
} else if (ctx.target === "draft-07") {
result.$schema = "http://json-schema.org/draft-07/schema#";
} else if (ctx.target === "draft-04") {
result.$schema = "http://json-schema.org/draft-04/schema#";
} else if (ctx.target === "openapi-3.0") {
// OpenAPI 3.0 schema objects should not include a $schema property
} else {
// Arbitrary string values are allowed but won't have a $schema property set
}
if (ctx.external?.uri) {
const id = ctx.external.registry.get(schema)?.id;
if (!id) throw new Error("Schema is missing an `id` property");
result.$id = ctx.external.uri(id);
}
Object.assign(result, root.def ?? root.schema);
// The `id` in `.meta()` is a Zod-specific registration tag used to extract
// schemas into $defs — it is not user-facing JSON Schema metadata. Strip it
// from the output body where it would otherwise leak. The id is preserved
// implicitly via the $defs key (and via $ref paths).
const rootMetaId = ctx.metadataRegistry.get(schema)?.id;
if (rootMetaId !== undefined && result.id === rootMetaId) delete result.id;
// build defs object
const defs: JSONSchema.BaseSchema["$defs"] = ctx.external?.defs ?? {};
for (const entry of ctx.seen.entries()) {
const seen = entry[1];
if (seen.def && seen.defId) {
if (seen.def.id === seen.defId) delete seen.def.id;View on GitHub (pinned to 2d90846af9)