how do you get a deep partial of a Zod nested schema, such that all feilds and nested objects are optional?

1k views Asked by At

Here is my example code

import { z, ZodTypeAny, ZodObject } from 'zod';

function deepShapeLog(schemanName: string, schema: ZodTypeAny, indent = 0) {
    const indentStr = ' '.repeat(indent);
    console.log(`${indentStr}${schemanName} : ${schema._def.typeName}`);
    if (schema instanceof ZodObject) {
        for (const key in schema.shape) {
            deepShapeLog(key, schema.shape[key], indent + 4);
        }
    }
}

const deepObj1 = z.object({
    obj1: z.object({
        obj2: z.object({
            obj3: z.object({
                field4: z.string(),
                field5: z.string()
            }),
            field6: z.number(),
        }),
        field7: z.boolean()
    }),
    field8: z.date(),
})

deepShapeLog('deepObj1', deepObj1);
const deepObj1Partial = deepObj1.deepPartial();
deepShapeLog('\ndeepObj1Partial', deepObj1Partial);

/* output from node v20.8.0 and zod 3.22.4
    deepObj1 : ZodObject
        obj1 : ZodObject
            obj2 : ZodObject
                obj3 : ZodObject
                    field4 : ZodString
                    field5 : ZodString
                field6 : ZodNumber
            field7 : ZodBoolean
        field8 : ZodDate

    deepObj1Partial : ZodObject
        obj1 : ZodOptional
        field8 : ZodOptional
*/

And I want to take an exact copy of the structure with all fields and nested objects set to optional.

I've tried ZodObject.deepPartial, however, this lost the fields in my nested objects.

I've tried creating a recursive function, however, it seems that the copy constructor of the ZodObject loses the fields of nested objects, so as the recursion unfolds the sub-collections are lost.

I've spent 8 hours on this (i'm ashamed to say), I've even tried writing a nun recursive function... I just don't understand why this would be so hard, I'm clearly missing something.

The weird thing is that the vs code type script compiler thinks everything is fine and gives me the following hint: vs code hint of deepObjPartial

however at run time all the sub shape is gone...

1

There are 1 answers

0
Philip Wilson On

writing some example code really helped me understand a principle of zod that had just past me by.

I had thought that optionality was just an attribute of the field, however it turns out (and I'm sure for good reason) that it actually wraps the object in an optional type... and you need to unwrap it to access the actual feild... just like the docs say.

here is an updated deepShapeLog and same example above:

import { z, ZodTypeAny, ZodObject, ZodOptional } from 'zod';
function deepShapeLog(schemanName: string, schema: ZodTypeAny, indent = 0) {
    const indentStr = ' '.repeat(indent);
    console.log(`${indentStr}${schemanName} : ${schema._def.typeName} ${schema._def.innerType ? schema._def.innerType._def.typeName : ''}`);
    if (schema instanceof ZodOptional) {
        schema = schema.unwrap();
    }
    if (schema instanceof ZodObject) {
        for (const key in schema.shape) {
            deepShapeLog(key, schema.shape[key], indent + 4);
        }
    }
}

const deepObj1 = z.object({
    obj1: z.object({
        obj2: z.object({
            obj3: z.object({
                field4: z.string(),
                field5: z.string()
            }),
            field6: z.number(),
        }),
        field7: z.boolean()
    }),
    field8: z.date(),
})

deepShapeLog('deepObj1', deepObj1);
const deepObj1Partial = deepObj1.deepPartial();
deepShapeLog('\ndeepObj1Partial', deepObj1Partial);

/* output rom node v20.8.0 and zod 3.22.4
deepObj1 : ZodObject 
    obj1 : ZodObject 
        obj2 : ZodObject 
            obj3 : ZodObject 
                field4 : ZodString 
                field5 : ZodString 
            field6 : ZodNumber 
        field7 : ZodBoolean 
    field8 : ZodDate 

deepObj1Partial : ZodObject 
    obj1 : ZodOptional ZodObject
        obj2 : ZodOptional ZodObject
            obj3 : ZodOptional ZodObject
                field4 : ZodOptional ZodString
                field5 : ZodOptional ZodString
            field6 : ZodOptional ZodNumber
        field7 : ZodOptional ZodBoolean
    field8 : ZodOptional ZodDate
*/

As I said in the post, I was clearly missing something... now I know what it is! Can't believe it took me more than 8 hours to work that out though... :-/

hopefully this will save someone else those 8 hours...