Open hornta opened 1 year ago
I also think it would be worthwhile to have this in the API (and possibly deepPassthrough()
and deepStrip()
).
If you're in a hurry, I think you can implement it yourself. Take a look at deepPartial
+deepPartialify
for inspiration.
deepPartial(): partialUtil.DeepPartial<this> {
return deepPartialify(this) as any;
}
function deepPartialify(schema: ZodTypeAny): any {
if (schema instanceof ZodObject) {
const newShape: any = {};
for (const key in schema.shape) {
const fieldSchema = schema.shape[key];
newShape[key] = ZodOptional.create(deepPartialify(fieldSchema));
}
return new ZodObject({
...schema._def,
shape: () => newShape,
}) as any;
} else if (schema instanceof ZodArray) {
return ZodArray.create(deepPartialify(schema.element));
} else if (schema instanceof ZodOptional) {
return ZodOptional.create(deepPartialify(schema.unwrap()));
} else if (schema instanceof ZodNullable) {
return ZodNullable.create(deepPartialify(schema.unwrap()));
} else if (schema instanceof ZodTuple) {
return ZodTuple.create(
schema.items.map((item: any) => deepPartialify(item))
);
} else {
return schema;
}
}
I've tried to generalize to all kind of deep unknown policies. Here's what I got:
First, you need 2 utilities: a function and a type.
type ZodObjectMapper<T extends ZodRawShape, U extends UnknownKeysParam> = (
o: ZodObject<T>
) => ZodObject<T, U>;
function deepApplyObject(
schema: ZodTypeAny,
map: ZodObjectMapper<any, any>
): any {
if (schema instanceof ZodObject) {
const newShape: Record<string, ZodTypeAny> = {};
for (const key in schema.shape) {
const fieldSchema = schema.shape[key];
newShape[key] = deepApplyObject(fieldSchema, map);
}
const newObject = new ZodObject({
...schema._def,
shape: () => newShape,
});
return map(newObject);
} else if (schema instanceof ZodArray) {
return ZodArray.create(deepApplyObject(schema.element, map));
} else if (schema instanceof ZodOptional) {
return ZodOptional.create(deepApplyObject(schema.unwrap(), map));
} else if (schema instanceof ZodNullable) {
return ZodNullable.create(deepApplyObject(schema.unwrap(), map));
} else if (schema instanceof ZodTuple) {
return ZodTuple.create(
schema.items.map((item: any) => deepApplyObject(item, map))
);
} else {
return schema;
}
}
type DeepUnknownKeys<
T extends ZodTypeAny,
UnknownKeys extends UnknownKeysParam
> = T extends ZodObject<infer Shape, infer _, infer Catchall>
? ZodObject<
{
[k in keyof Shape]: DeepUnknownKeys<Shape[k], UnknownKeys>;
},
UnknownKeys,
Catchall
>
: T extends ZodArray<infer Type, infer Card>
? ZodArray<DeepUnknownKeys<Type, UnknownKeys>, Card>
: T extends ZodOptional<infer Type>
? ZodOptional<DeepUnknownKeys<Type, UnknownKeys>>
: T extends ZodNullable<infer Type>
? ZodNullable<DeepUnknownKeys<Type, UnknownKeys>>
: T extends ZodTuple<infer Items>
? {
[k in keyof Items]: Items[k] extends ZodTypeAny
? DeepUnknownKeys<Items[k], UnknownKeys>
: never;
} extends infer PI
? PI extends ZodTupleItems
? ZodTuple<PI>
: never
: never
: T;
Then we only need to define those 3 functions + types:
type DeepPassthrough<T extends ZodTypeAny> = DeepUnknownKeys<T, 'passthrough'>;
function deepPassthrough<T extends ZodTypeAny>(schema: T): DeepPassthrough<T> {
return deepApplyObject(schema, (s) => s.passthrough()) as DeepPassthrough<T>;
}
type DeepStrip<T extends ZodTypeAny> = DeepUnknownKeys<T, 'strip'>;
function deepStrip<T extends ZodTypeAny>(schema: T): DeepStrip<T> {
return deepApplyObject(schema, (s) => s.strip()) as DeepStrip<T>;
}
type DeepStrict<T extends ZodTypeAny> = DeepUnknownKeys<T, 'strict'>;
function deepStrict<T extends ZodTypeAny>(
schema: T,
error?: errorUtil.ErrMessage
): DeepStrict<T> {
return deepApplyObject(schema, (s) => s.strict(error)) as DeepStrict<T>;
}
Note that I've left some any
in the definitions above. Open to any suggestion to fix this.
And that's it. Sample usage:
const schema = z.object({
a: z.string(),
b: z.object({
a: z.string(),
b: z.array(
z.object({
a: z.string(),
})
),
}),
});
const value = {
a: 'value',
b: {
a: 'value',
b: [{ a: 'value' }, { a: 'value', c: 'unknown' }],
c: 'unknown',
},
c: 'unknown',
};
const stripSchema = deepStrip(schema);
console.log(JSON.stringify(stripSchema.parse(value)));
// => {"a":"value","b":{"a":"value","b":[{"a":"value"},{"a":"value"}]}}
const passthroughSchema = deepPassthrough(schema);
console.log(JSON.stringify(passthroughSchema.parse(value)));
// => {"a":"value","b":{"a":"value","b":[{"a":"value"},{"a":"value","c":"unknown"}],"c":"unknown"},"c":"unknown"}
const strictSchema = deepStrict(schema, 'ERROR');
console.log(JSON.stringify(strictSchema.parse(value)));
// => throws
Caveat: the implementation provided by @gawi suffers the same shortcomings as deepPartial
:
Important limitation: deep partials only work as expected in hierarchies of objects, arrays, and tuples.
The policy of a recursive type (defined with z.lazy
) cannot be changed with the current implemetation of deepApplyObject
, it would need to be enhanced to handle a schema of type ZodType<T, ZodTypeDef, U>
, it doesn't look like a trivial change.
deepPartial
is very fragile and probably shouldn't have been added.
Zod intentionally avoids "modes" and contextual validation - it's too hard to reason about. Using z.strictObject
everywhere is the recommended approach. You might be able to make a custom lint rule to prevent any usage of z.object
. If you do that, ping me and I'll add it to the readme so others can use it too.
For consuming APIs I would almost always pick z.object, but for data exploration as I am doing now z.strictObject is necessary and I am glad I found about it. Of course if you want highly focused lib (on consuing known APIs) with great DX than it may not make sense to add it. Just sharing my pov.
TypeScript's structural-based type system makes it easy to accidentally leak data (e.g. include password hashes in an HTTP response after fetching a User row from a database table) when building web applications. Thanks to this issue, I know about z.object().strict()
/z.strictObject()
and strict parsing is a great way to work around some of these risks: I can parse responses prior to returning them to the user and fail if there are extra fields.
Defining all the objects to be .strictObject
is not ideal in some circumstances. It can make changes hard. For instance, a web client + web server pair need to be deployed at the exact same time if they are sharing their zod schema. Alternatively, the schema's new fields need to first be made optional, requiring multiple deploys to reach the end state.
I'm therefore writing this comment to +1 the idea of adding .deepStrict()
or having a strictParse()
which behaves like parse()
but considers object
to be strictObject
. Backend could expose non-strict schemas but use strict schemas to validate data prior to sending responses. In a similar way, frontends could implement strict schema parsing prior to making a request but the server can accept additional fields.
P.S. I'm a long time zod fan, first time sharing some feedback -- thanks a lot of writing and maintaining this high quality library over the years!
Hi, I would like to either have strict on all of my types or something like
.deepStrict()
that propagates down on all sub objects in my schema. Now it's too easy to miss out on defining a schema as strict I think.