How to recursively map objects as strict objects? #5627
Replies: 2 comments
|
Hi @branko-toshin-deel! I'm Dosu and I’m helping the zod team. I'm a bot trained on Zod's codebase and issues. There isn't a built-in Zod method to recursively apply Here's a simplified version of such a utility, adapted from community examples: import { z, ZodTypeAny, ZodObject, ZodArray, ZodUnion, ZodOptional, ZodNullable, ZodTuple, ZodEffects } from "zod";
function deepStrict<T extends ZodTypeAny>(schema: T): T {
if (schema instanceof ZodObject) {
const shape = schema.shape;
const newShape: any = {};
for (const key in shape) {
newShape[key] = deepStrict(shape[key]);
}
return z.object(newShape).strict() as any;
}
if (schema instanceof ZodArray) {
return z.array(deepStrict(schema.element)) as any;
}
if (schema instanceof ZodUnion) {
return z.union(schema.options.map(deepStrict)) as any;
}
if (schema instanceof ZodOptional) {
return z.optional(deepStrict(schema.unwrap())) as any;
}
if (schema instanceof ZodNullable) {
return z.nullable(deepStrict(schema.unwrap())) as any;
}
if (schema instanceof ZodTuple) {
return z.tuple(schema.items.map(deepStrict)) as any;
}
if (schema instanceof ZodEffects) {
return new ZodEffects({
...schema._def,
schema: deepStrict(schema._def.schema),
}) as any;
}
return schema;
}This utility covers most cases, but there are caveats: it doesn't handle recursive schemas defined with For your two-step validation: use your original (loose) schema for the required-data check, and the deep-strict version for logging extra keys. For example: const looseResult = userSchema.safeParse(data);
if (!looseResult.success) throw new Error("API schema mismatch");
const strictSchema = deepStrict(userSchema);
const strictResult = strictSchema.safeParse(data);
if (!strictResult.success) {
// Log extra keys here (e.g., to Sentry), but don't throw
}
return looseResult.data;If this answers your question, please close the issue! To reply, just mention @dosu. Share context across your team and agents. Try Dosu. |
|
There is no built-in for it, but you do not have to special-case every wrapper by hand either. import * as z from "zod";
const CHILD_KEYS: Record<string, string[]> = {
array: ["element"], set: ["valueType"], map: ["keyType", "valueType"],
record: ["keyType", "valueType"], union: ["options"],
intersection: ["left", "right"], tuple: ["items", "rest"],
optional: ["innerType"], nullable: ["innerType"], nonoptional: ["innerType"],
default: ["innerType"], prefault: ["innerType"], catch: ["innerType"],
readonly: ["innerType"], promise: ["innerType"], pipe: ["in", "out"],
};
function deepStrict<T extends z.core.$ZodType>(schema: T, seen = new Map()): T {
if (seen.has(schema)) return seen.get(schema);
const def: any = (schema as any)._zod.def;
if (def.type === "object") {
const shape: any = {};
let result: any;
seen.set(schema, z.lazy(() => result)); // stand-in while the cycle is open
for (const [k, v] of Object.entries(def.shape)) shape[k] = deepStrict(v as any, seen);
result = z.strictObject(shape);
seen.set(schema, result);
return result;
}
if (def.type === "lazy") {
const result = z.lazy(() => deepStrict(def.getter(), seen));
seen.set(schema, result);
return result as any;
}
const keys = CHILD_KEYS[def.type];
if (!keys) return schema;
const next: any = { ...def };
for (const key of keys) {
const v = def[key];
if (v == null) continue;
next[key] = Array.isArray(v) ? v.map((x: any) => deepStrict(x, seen)) : deepStrict(v, seen);
}
const result = z.core.clone(schema, next);
seen.set(schema, result);
return result as any;
}On your schema it rejects an unknown key at any depth, including inside the union branches, and const Strict = deepStrict(userSchema);
Strict.safeParse({ name: "a", age: 1, hobbies: [{ name: "x", frequency: 1 }, { name: "y" }] }).success; // true
Strict.safeParse({ name: "a", age: 1, hobbies: [{ name: "x", frequency: 1, extra: 1 }, { name: "y" }] }).success; // false
Strict.safeParse({ name: "a", age: 1, hobbies: [{ name: "y" }] }).success; // false, still .min(2)
userSchema.safeParse({ name: "a", age: 1, hobbies: [{ name: "x", frequency: 1, extra: 1 }] }).success; // true, original untouchedThe For the two-pass check you describe, keep the original for pass one and this for pass two, and read |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
lets say I have a schema like below:
const weekdayHobbySchema = z.object({ name: z.string(), frequency: z.number() })const weekendHobbySchema = z.object({ name: z.string() })const userSchema = z.object({ name: z.string(), age: z.number(), hobbies: z.array(z.union([weekdayHobbySchema, weekendHobbySchema])).min(2) })I would like to write a utility function which will map over all object properties and map it as strictObject. While creating a simple utility function is not that hard, things get complicated when I want to preserve unions, array modifiers, transforms etc.
Is there a built in way to to this?
Reasoning behind this is straight forward. While parsing the data which is coming from the backend to the UI I want to perform 2 checks:
For this you can imagine having a code like this
if(z.safeParse(looseSchema, data).success === false) throw new Error ('API schema mismatch) if(z.safeParse(applyStrictRecursively(looseSchema)).success === false) // fire sentry log or something but do not throw an error, just return the data from the check #1Please let me know if this makes sense
All reactions