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

  1. Register the root schema in the external registry with an id before conversion: registry.add(schema, { id: "mySchema" }).
  2. Ensure ctx.external.registry is the same registry the schema was registered against.
  3. If you do not need external URIs, omit the external option entirely.
  4. 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

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


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)