colinhacks/zod · error · Error

Schema is missing an `id` property

Error message

Schema is missing an `id` property

What it means

Thrown near the end of `z.toJSONSchema()` when an `external.uri` generator is configured but the root schema being converted has no registered `id`. The external-URI feature constructs the output `$id` from the schema's id; without one it cannot form the URI. The schema must be registered with an id (via `.meta({ id })` or an external registry) before conversion.

Source

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

    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 912f0f51b0)

Solutions

  1. Add an id to the schema: `schema.meta({ id: 'MyType' })` before converting.
  2. Register the schema in the external registry with an id: `registry.add(schema, { id: 'MyType' })`.
  3. Drop the `external.uri` option if you do not need cross-document `$id` emission.

Example fix

// before
const S = z.object({ a: z.string() });
z.toJSONSchema(S, { external: { uri: (id) => `https://x.com/${id}.json`, registry: new Map() } }); // throws

// after
const S = z.object({ a: z.string() }).meta({ id: 'Thing' });
z.toJSONSchema(S, { external: { uri: (id) => `https://x.com/${id}.json`, registry: new Map() } });
Defensive patterns

Strategy: validation

Validate before calling

function ensureHasId(schema) {
  const id = (schema as any)._zod?.def?.meta?.id;
  if (!id) throw new Error('schema needs .meta({ id }) before external URI conversion');
}

Type guard

function schemaHasId(s, registry): boolean {
  return !!(registry?.get(s)?.id ?? (s as any)._zod?.def?.meta?.id);
}

Try / catch

try {
  z.toJSONSchema(schema, { external: { uri, registry } });
} catch (e) {
  if (e instanceof Error && e.message === 'Schema is missing an `id` property') {
    // add schema.meta({ id: '...' }) or registry.add(schema, { id }) and retry
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling `z.toJSONSchema(schema, { external: { uri: (id) => `https://x.com/${id}.json`, registry } })` where `schema` has no id in the registry and no `.meta({ id })`.

Common situations: Setting up cross-document `$ref`s with an external registry but forgetting to tag the root schema; renaming a schema and dropping its id.

Related errors


AI-assisted analysis of colinhacks/zod@912f0f51b0 (2026-08-03). Data as JSON: /data/errors/e5daad3e436f0113.json. Report an issue: GitHub.