colinhacks/zod · error · Error

Invalid element at key

Error message

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

What it means

Thrown by normalizeDef() while initializing a $ZodObject whenever a value in def.shape fails the check `def.shape[k]?._zod?.traits?.has("$ZodType")`. Every property of an object's shape must itself be a Zod schema instance carrying the $ZodType trait; passing a plain value, class, string, or non-schema object is rejected at construction time so that the object's parser can safely call _zod.run on each field.

Solutions

  1. Wrap every field value in a Zod schema (z.string(), z.number(), z.unknown(), etc.).
  2. If building the shape dynamically, guard each entry: if (!value?._zod?.traits?.has("$ZodType")) shape[k] = z.unknown();
  3. Check for undefined caused by circular imports — reorder imports or use z.lazy().
  4. Run a typecheck (tsc); the object() overload signature will usually flag mistyped fields before runtime.

Example fix

// before
const Schema = z.object({ name: "string", age: Number });
// after
const Schema = z.object({ name: z.string(), age: z.number() });
Defensive patterns

Strategy: type-guard

Validate before calling

function buildObject(shape: Record<string, unknown>) {
  const safe: Record<string, z.ZodType> = {};
  for (const [k, v] of Object.entries(shape)) {
    safe[k] = isZodSchema(v) ? v : z.unknown();
  }
  return z.object(safe);
}

Type guard

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

Try / catch

try {
  return z.object(shape as Record<string, z.ZodType>);
} catch (e) {
  throw new Error(`Object shape invalid: ${(e as Error).message}. Ensure every field is a Zod schema.`);
}

Prevention

When it happens

Trigger: Building z.object({ foo: "string" }), z.object({ foo: String }), z.object({ foo: { min: 1 } }), z.object({ foo: null }), or programmatically spreading a non-schema map into the shape. Also occurs when a spread/merge utility forgets to wrap a field, or when a lazy import resolves to undefined and is used as a field.

Common situations: Typing field values as native constructors (String, Number) instead of z.string()/z.number(); passing raw regex or plain option objects; circular imports that yield undefined at module-eval time; dynamic shape construction from external config without wrapping values in z.unknown() or similar.

Related errors


AI-assisted analysis of colinhacks/zod@2d90846af9 (2026-08-11). Data as JSON: /api/errors/4197025d4bd6c1a4. Report an issue: GitHub.

Appendix: source

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

  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 2d90846af9)