colinhacks/zod · error · Error

Invalid element at key "${k}": expected a Zod schema

Error message

Invalid element at key "${k}": expected a Zod schema

What it means

`normalizeDef` (schemas.ts:1838) walks every key of an object's shape and asserts each value has the `$ZodType` trait (`def.shape[k]._zod.traits.has("$ZodType")`). If any value is a plain object, primitive, class, or non-Zod schema, it throws `Invalid element at key "${k}"` at construction time. This catches mistakes where a field is given a raw value instead of a Zod schema.

Source

Thrown at packages/zod/src/v4/core/schemas.ts:1842

  propValues: util.PropValues;
  output: $InferObjectOutput<Shape, Config["out"]>;
  input: $InferObjectInput<Shape, Config["in"]>;
  optin?: "optional" | undefined;
  optout?: "optional" | undefined;
}
export type $ZodLooseShape = Record<string, any>;

export interface $ZodObject<
  /** @ts-ignore Cast variance */
  out Shape extends Readonly<$ZodShape> = Readonly<$ZodShape>,
  out Params extends $ZodObjectConfig = $ZodObjectConfig,
> extends $ZodType<any, any, $ZodObjectInternals<Shape, Params>> {}

function normalizeDef(def: $ZodObjectDef) {
  const keys = Object.keys(def.shape);
  for (const k of keys) {
    if (!def.shape?.[k]?._zod?.traits?.has("$ZodType")) {
      throw new Error(`Invalid element at key "${k}": expected a Zod schema`);
    }
  }
  const okeys = util.optionalKeys(def.shape);

  return {
    ...def,
    keys,
    keySet: new Set(keys),
    numKeys: keys.length,
    optionalKeys: new Set(okeys),
  };
}

function handleCatchall(
  proms: Promise<any>[],
  input: any,
  payload: ParsePayload,
  ctx: ParseContext,

View on GitHub (pinned to 912f0f51b0)

Solutions

  1. Wrap every field value in a Zod schema: `z.object({ foo: z.string() })`.
  2. For nested data use `z.object()` recursively rather than a plain object.
  3. Check the offending key named in the error message and ensure its value is a Zod schema instance.

Example fix

// before
const S = z.object({ name: "string", count: 0 }); // throws at 'name'
// after
const S = z.object({ name: z.string(), count: z.number() });
Defensive patterns

Strategy: validation

Validate before calling

import { z } from "zod";

function isZodSchema(v: unknown): v is z.ZodType {
  return !!v && typeof v === "object" && !!(v as any)?._zod?.traits?.has("$ZodType");
}

function assertObjectShape(shape: Record<string, unknown>) {
  for (const [k, v] of Object.entries(shape)) {
    if (!isZodSchema(v)) {
      throw new Error(`Invalid element at key "${k}": expected a Zod schema`);
    }
  }
}

Type guard

import { z } from "zod";

function isZodSchema(v: unknown): v is z.ZodType {
  return !!v && typeof v === "object" && !!(v as any)?._zod?.traits?.has("$ZodType");
}

// Usage: Object.values(shape).every(isZodSchema)

Prevention

When it happens

Trigger: Constructing `z.object({ foo: "string" })`, `z.object({ count: 0 })`, `z.object({ nested: { x: z.string() } })` (raw object instead of `z.object()`), or passing a Yup/Joi schema. Any value lacking `_zod.traits` triggers it.

Common situations: Forgetting the `z.` prefix (writing `string` instead of `z.string()`); nesting raw config objects; copy-pasting TypeScript types into runtime schemas; importing a schema that is actually `undefined` due to a bad import path.

Related errors


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