Is there a way to deepPick, similar to deepPartial? #2083
-
ProblemI want to pick deeply nested properties. Schema:const person = z.object({
name: z.string(),
school: z.object({
id: z.number(),
name: z.string()
})
}); Desired solutionconst justSchoolName = person.deepPick({
school: {
name: true
}
}); |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 7 replies
-
Is this what you are looking for? const person = z.object( {
name: z.string(),
school: z.object( {
id: z.number(),
name: z.string()
} )
} )
const justSchoolName = person.shape.school.pick( {
name: true
} )
console.log( justSchoolName.parse( { name: 'School of Rock' } ) )
// { name: 'School of Rock' } If you found my answer satisfactory, please consider supporting me. Even a small amount is greatly appreciated. Thanks friend! 🙏 |
Beta Was this translation helpful? Give feedback.
-
@JacobWeisenburger Not exactly what I'm looking for. It would be very nice to have a built-in However, your answer did help me solve the problem. I ended up writing a recursive util function that takes a schema and a property path, and returns the deeply nested schema. It looks something like this: My solution:function deepPick(schema: ZodObject, propertyPath: string) {
const properties = propertyPath.split('.');
if (properties.length === 0) {
return schema;
}
const currentProperty = properties[0];
if (properties.length === 1) {
return schema.pick({ [currentProperty]: true });
}
const nextPropertyPath = properties.slice(1, properties.length).join('.');
const nextSchema = schema.shape[currentProperty];
if (!nextSchema) return schema;
return deepPick(nextSchema, nextPropertyPath);
} Usage:const person = z.object({
name: z.string(),
school: z.object({
id: z.number(),
name: z.string()
})
});
const justSchoolName = deepPick(person, 'school.name'); Result:console.log(justSchoolName.shape); // { name: ZodString } I will be closing this issue. Thanks for your help, Jacob. |
Beta Was this translation helpful? Give feedback.
-
It is absolutely doable with userland code, but needs some tweak with typescript. Hopefully it can be implemented into the library. The following code handles masks like z.object({
a: z.string(),
b: z.string(),
c: z.object({ m: z.string(), n: z.string() }),
}); Of course you can also pick the entire Nullable and optional will be picked for its inner object, if contains one. Array, union and intersection types are not handled. import {
UnknownKeysParam,
ZodNullable,
ZodObject,
ZodOptional,
ZodRawShape,
ZodTypeAny,
} from 'zod';
export type UnwrapNullish<T extends ZodTypeAny> = T extends ZodNullable<infer I>
? UnwrapNullish<I>
: T extends ZodOptional<infer I>
? UnwrapNullish<I>
: T;
export const unwrapNullish = <T extends ZodTypeAny>(t: T): UnwrapNullish<T> => {
if (t instanceof ZodNullable || t instanceof ZodOptional) {
return unwrapNullish(t.unwrap());
}
return t as any;
};
export type ReplaceNullish<N extends ZodTypeAny, Inner extends ZodTypeAny> = N extends ZodNullable<
infer I
>
? ZodNullable<ReplaceNullish<I, Inner>>
: N extends ZodOptional<infer I>
? ZodOptional<ReplaceNullish<I, Inner>>
: Inner;
export const replaceNullish = <N extends ZodTypeAny, Inner extends ZodTypeAny>(
t: ZodTypeAny,
inner: Inner,
): ReplaceNullish<N, Inner> => {
if (t instanceof ZodNullable) {
return replaceNullish(t.unwrap(), inner).nullable() as any;
} else if (t instanceof ZodOptional) {
return replaceNullish(t.unwrap(), inner).optional() as any;
} else {
return inner as any;
}
};
type DeepPickMaskInner<
T extends ZodTypeAny,
U = UnwrapNullish<T>,
> = U extends ZodObject<ZodRawShape>
? {
[K in keyof U['shape']]?: DeepPickMaskInner<U['shape'][K]> | true;
}
: true;
export type DeepPickMask<T extends ZodRawShape> = DeepPickMaskInner<ZodObject<T>>;
export type DeepPickMaskKeys<
T extends ZodRawShape,
Mask extends DeepPickMask<T> = DeepPickMask<T>,
Keys extends keyof T = keyof T,
> = Keys extends keyof Mask
? Mask[Keys] extends true
? Keys
: Mask[Keys] extends {}
? Keys
: never
: never;
type ReplaceShape<
T extends ZodObject<ZodRawShape>,
Shape extends ZodRawShape,
> = T extends ZodObject<
// eslint-disable-next-line @typescript-eslint/no-unused-vars
infer _,
infer UnknownKeys extends UnknownKeysParam,
infer Catchall extends ZodTypeAny,
infer Output,
infer Input
>
? ZodObject<Shape, UnknownKeys, Catchall, Output, Input>
: never;
export type DeepPickShape<T extends ZodRawShape, Mask extends DeepPickMask<T> = DeepPickMask<T>> = {
[K in DeepPickMaskKeys<T, Mask>]: K extends keyof Mask
? Mask[K] extends true
? T[K]
: Mask[K] extends {}
? T[K] extends ZodObject<ZodRawShape>
? ReplaceShape<T[K], DeepPickShape<T[K]['shape'], Mask[K]>>
: T[K] extends ZodNullable<infer U>
? UnwrapNullish<U> extends ZodObject<infer Shape extends ZodRawShape>
? ReplaceNullish<
T[K],
ReplaceShape<UnwrapNullish<U>, DeepPickShape<Shape, Mask[K] & DeepPickMask<Shape>>>
>
: T[K]
: T[K] extends ZodOptional<infer U>
? UnwrapNullish<U> extends ZodObject<infer Shape extends ZodRawShape>
? ReplaceNullish<
T[K],
ReplaceShape<UnwrapNullish<U>, DeepPickShape<Shape, Mask[K] & DeepPickMask<Shape>>>
>
: T[K]
: never
: never
: never;
};
export type DeepPick<
O extends ZodObject<ZodRawShape>,
Mask extends DeepPickMask<O['shape']>,
> = DeepPickShape<O['shape'], Mask> extends infer S extends ZodRawShape
? ReplaceShape<O, S>
: never;
/**
* @example
* ```ts
* const picked = deepPick(o, {
* a: true,
* nested: {
* c: true,
* },
* });
* ```
* @param type the ZodObject to pick deeply
* @param mask the mask to pick deeply.
* @returns the picked schema.
* Note: optional and nullable fields will be picked for its inner type.
*/
export const deepPick = <
O extends ZodObject<ZodRawShape>,
const Mask extends DeepPickMask<O['shape']>,
>(
type: O,
mask: Mask,
): DeepPick<O, Mask> => {
const keys: Record<string, true> = {};
for (const [key, value] of Object.entries(mask)) {
if (value === true) {
keys[key] = true;
}
}
let current = type.pick(keys);
for (const [key, value] of Object.entries(mask)) {
if (typeof value === 'object') {
let sourceSchema = type.shape[key];
let pickedSchema: any;
if (sourceSchema instanceof ZodObject) {
pickedSchema = deepPick(sourceSchema, value);
} else if (sourceSchema instanceof ZodNullable) {
pickedSchema = replaceNullish(
sourceSchema,
deepPick(unwrapNullish(sourceSchema) as any, value),
);
} else if (sourceSchema instanceof ZodOptional) {
pickedSchema = replaceNullish(
sourceSchema,
deepPick(unwrapNullish(sourceSchema) as any, value),
);
} else {
throw new Error(`Cannot pick ${sourceSchema?.constructor?.name}. `);
}
current = current.extend({
[key]: pickedSchema,
});
}
}
return current as any;
}; |
Beta Was this translation helpful? Give feedback.
-
Here is my solution! It was heavily inspired by @DoubleDebug's solution. The only difference is that it has a bit of logic to handle arrays. My Solutionfunction isZodObject(schema: z.ZodTypeAny): schema is z.AnyZodObject {
if (schema._def.typeName === "ZodObject") return true;
return false;
}
function isZodArray(schema: z.ZodTypeAny): schema is z.ZodArray<any> {
if (schema._def.typeName === "ZodArray") return true;
return false;
}
function pickObject(schema: z.ZodTypeAny, path: string): z.ZodTypeAny {
if (!isZodObject(schema)) throw Error("Not a zod object");
const newSchema = schema.shape?.[path];
if (!newSchema) throw Error(`${path} does not exist on schema with keys: ${Object.keys(schema.shape)}`);
return newSchema;
}
function pickArray(schema: z.ZodTypeAny): z.ZodTypeAny {
if (!isZodArray(schema)) throw Error("Not a Zod Array");
const newSchema = schema?.element;
if (!newSchema) throw Error("No element on Zod Array");
return newSchema;
}
function zodDeepPick(schema: z.ZodTypeAny, propertyPath: string): z.ZodTypeAny {
if (propertyPath === "") return schema;
const numberRegex = new RegExp("[[0-9]+]");
const arrayIndex = propertyPath.search(numberRegex);
const objectIndex = propertyPath.indexOf(".");
const matchedArray = arrayIndex !== -1;
const matchedObject = objectIndex !== -1;
if ((matchedArray && matchedObject && arrayIndex < objectIndex) || (matchedArray && !matchedObject)) {
const arraySplit = propertyPath.split(numberRegex);
const restArray = arraySplit.slice(1, arraySplit.length).join("[0]");
if (arrayIndex !== 0) {
return zodDeepPick(pickObject(schema, arraySplit[0]), `[0]${restArray}`);
}
return zodDeepPick(
pickArray(schema),
restArray.charAt(0) === "." ? restArray.slice(1, restArray.length) : restArray
);
}
if (matchedObject) {
const objectSplit = propertyPath.split(".");
const restObject = objectSplit.slice(1, objectSplit.length).join(".");
return zodDeepPick(pickObject(schema, objectSplit[0]), restObject);
}
return pickObject(schema, propertyPath);
} Usage const schema = z.object({
fake: z.object({
thing: z.array(z.string()),
foo: z.array(z.array(z.string().email())),
peanut: z.array(
z.object({
butter: z.number(),
test: z.array(z.boolean()),
testing: z.array(
z.object({
hi: z.array(z.string()),
})
),
})
),
}),
});
zodDeepPick(schema, "fake.thing[69]") // z.string()
zodDeepPick(schema, "fake.foo[69][23]") // z.string().email()
zodDeepPick(schema, "fake.peanut[32].butter") // z.number()
zodDeepPick(schema, "fake.peanut[0].test[420]") // z.boolean()
zodDeepPick(schema, "fake.peanut[32].testing[432].hi") // z.array(z.string())
zodDeepPick(schema, "fake.notreal") // throws Error: notreal does not exist on schema with keys: thing,foo,peanut
zodDeepPick(schema, "fake[0]") // throws Error: Not a Zod Array |
Beta Was this translation helpful? Give feedback.
Is this what you are looking for?
If you found my answer satisfactory, please consider supporting me. Even a small amount is greatly appreciated. Thanks friend! 🙏
https://github.com/sponsors/JacobWeisenburger