9

I'm pretty sure this exists, but I haven't been able to find anything about it despite some digging. Say that I have a zod Schema like such:

const Person = zod.object({
    name: z.string().default(''),
    age: z.number().nullable();
});

Is there a way to create something like this:

const InstancePerson = {
    name: '',
    age: null
}

from the zod Schema?

Dragonsnap
  • 834
  • 10
  • 25

2 Answers2

10

I know that I am a little late to the party but maybe it will help someone in the future.

You could extend your zod schema in the following way:

const Person = zod.object({
    name: z.string().default(''),
    age: z.number().nullable().default(null)
}).default({}); // .default({}) could be omitted in this case but should be set in nested objects

Now, you can retrieve your desired output by calling:

const InstancePerson = Person.parse({});
ChaRioteer
  • 116
  • 1
  • 5
4

There doesn't seem to be a direct way to do this sort of thing from the library, but you can dig into their _ private fields and get the functionality you're looking for.

There are some risks associated with this approach because library maintainers typically don't guarantee stability in these private properties. If you're relying on this behavior you may need to be extra careful about version bumps.

Ok disclaimer out of the way, something like this is possible. Extending this to more types is left as an exercise for the reader:

import { z } from "zod";

const schema = z.object({
  name: z.string(),
  age: z.number().nullable()
});

const schemaDefaults = <Schema extends z.ZodFirstPartySchemaTypes>(
  schema: Schema
): z.TypeOf<Schema> => {
  switch (schema._def.typeName) {
    case z.ZodFirstPartyTypeKind.ZodDefault:
      return schema._def.defaultValue();
    case z.ZodFirstPartyTypeKind.ZodObject: {
      // The switch wasn't able to infer this but the cast should
      // be safe.
      return Object.fromEntries(
        Object.entries(
          (schema as z.SomeZodObject).shape
        ).map(([key, value]) => [key, schemaDefaults(value)])
      );
    }
    case z.ZodFirstPartyTypeKind.ZodString:
      return "";
    case z.ZodFirstPartyTypeKind.ZodNull:
      return null;
    case z.ZodFirstPartyTypeKind.ZodNullable:
      return null;
    // etc
    default:
      throw new Error(`Unsupported type ${schema._type}`);
  }
};

console.log(schemaDefaults(schema));

Here, I've specified no defaults but the code still outputs what you expected. If you specified "foo" as the default for name the code will output { name: "foo", age: null }

A shorter approach would be to simply dig in one layer into the _def of your schema looking for defaultValue functions to call, but I think the given approach is more principled since it could be extended to support every core zod schema type.

One last word of warning, some of the zod types are not as straightforward to handle as others. Something like z.number could be reasonably given a default of 0, but z.union or z.intersection would have interesting recursive cases.

It might be worth building out a library just for this handling or else opening an issue with the repo to make it part of the offered api.

Souperman
  • 5,057
  • 1
  • 14
  • 39
  • 1
    Pretty impractical due to the limitations for unions & intersections, but still a solution. Thanks for the effort. – Dragonsnap Jul 04 '22 at 06:28
  • I am really struggling here. Not only the unions, intersections, but also arrays and it especially does not work if you specify extra zod functions after an object like z.object({name: z.string, age: z.number}).describe('user'), the 'describe' is last zod object referred to and not not the upper zod object, so the recursion of the zod object only works without extra piped zod functions. Would it be a good idea to try to detect the actual top level ...def.innerType and then start testing what datatype actually needs to be generated? – Ronald Brinkerink Feb 07 '23 at 14:44
  • Yeah that's what's rough about this approach. You'll essentially need to handle each type of zod object that can arise which means you'll need to be in lock step with the library and aware of the functions. I would say, unless you yourself are trying to make a library to handle this generically for anyone using zod, I would implement these features a la carte and throw in unimplemented cases to keep your sanity. What you're describing sounds like a viable option (but entails work) – Souperman Feb 08 '23 at 01:25
  • 1
    Your solution is perfect for simple DTOs. I just added cases handlers for numbers, boolean, date and array. It helped me reducing code for creating default values for forms. – Adnan Aug 09 '23 at 21:14