{"record":{"id":"e5daad3e436f0113","repo":"colinhacks/zod","slug":"schema-is-missing-an-id-property","errorCode":null,"errorMessage":"Schema is missing an `id` property","messagePattern":"Schema is missing an `id` property","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/zod/src/v4/core/to-json-schema.ts","lineNumber":469,"sourceCode":"    flattenRef(entry[0]);\n  }\n\n  const result: JSONSchema.BaseSchema = {};\n  if (ctx.target === \"draft-2020-12\") {\n    result.$schema = \"https://json-schema.org/draft/2020-12/schema\";\n  } else if (ctx.target === \"draft-07\") {\n    result.$schema = \"http://json-schema.org/draft-07/schema#\";\n  } else if (ctx.target === \"draft-04\") {\n    result.$schema = \"http://json-schema.org/draft-04/schema#\";\n  } else if (ctx.target === \"openapi-3.0\") {\n    // OpenAPI 3.0 schema objects should not include a $schema property\n  } else {\n    // Arbitrary string values are allowed but won't have a $schema property set\n  }\n\n  if (ctx.external?.uri) {\n    const id = ctx.external.registry.get(schema)?.id;\n    if (!id) throw new Error(\"Schema is missing an `id` property\");\n    result.$id = ctx.external.uri(id);\n  }\n\n  Object.assign(result, root.def ?? root.schema);\n\n  // The `id` in `.meta()` is a Zod-specific registration tag used to extract\n  // schemas into $defs — it is not user-facing JSON Schema metadata. Strip it\n  // from the output body where it would otherwise leak. The id is preserved\n  // implicitly via the $defs key (and via $ref paths).\n  const rootMetaId = ctx.metadataRegistry.get(schema)?.id;\n  if (rootMetaId !== undefined && result.id === rootMetaId) delete result.id;\n\n  // build defs object\n  const defs: JSONSchema.BaseSchema[\"$defs\"] = ctx.external?.defs ?? {};\n  for (const entry of ctx.seen.entries()) {\n    const seen = entry[1];\n    if (seen.def && seen.defId) {\n      if (seen.def.id === seen.defId) delete seen.def.id;","sourceCodeStart":451,"sourceCodeEnd":487,"githubUrl":"https://github.com/colinhacks/zod/blob/2d90846af918af9602e088812d63a035d47cdbe4/packages/zod/src/v4/core/to-json-schema.ts#L451-L487","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst reg = z.registry();\nconst json = z.toJSONSchema(MySchema, { external: { registry: reg, uri: (id) => `/${id}` } });\n// after\nconst reg = z.registry();\nreg.add(MySchema, { id: \"mySchema\" });\nconst json = z.toJSONSchema(MySchema, { external: { registry: reg, uri: (id) => `/${id}` } });","handlingStrategy":"validation","validationCode":"function toJSONSchemaExternal(schema: z.ZodType, registry: z.ZodRegistry) {\n  const id = registry.get(schema)?.id;\n  if (!id) throw new Error(\"Schema must be registered with an id before external conversion.\");\n  return z.toJSONSchema(schema, { external: { registry, uri: (i) => `/${i}` } });\n}","typeGuard":"function hasRegistryId(schema: z.ZodType, registry: z.ZodRegistry): boolean {\n  return !!registry.get(schema)?.id;\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["json-schema","conversion","external","registry","id"],"backgroundTag":null,"analyzedSha":"2d90846af918af9602e088812d63a035d47cdbe4","analyzedAt":"2026-08-11T01:21:44.015Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}