Zod: Create a primitive object from defaults
Asked Answered
N

4

13

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?

Nogging answered 27/6, 2022 at 7:9 Comment(0)
S
12

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({});
Sharkey answered 9/9, 2022 at 14:40 Comment(2)
This don't work with validation rules. How to do this while avoiding them?Tuesday
Also this does throw errors when there is a non-default value, and Person.parse does not type the input.Demy
D
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.

Driskell answered 30/6, 2022 at 12:27 Comment(4)
Pretty impractical due to the limitations for unions & intersections, but still a solution. Thanks for the effort.Nogging
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?Maunsell
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)Driskell
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.Tuesday
D
3

So I have visited this thread a few times already and now I would like to give here my opinion,

because here is what my problem was/is:

I want to safely instantiate a new zod object by leveraging the defaults but also expecting me to input the required, none-defaults.

So the .parse functionality just accepts unknown and does not help me to understand what what I need to specify and what is a default.

Now if you have the same problem, where you want something like MyZodObject.createInstance then I might have a simple solution

The solution is z.input<typeof MyShape>

What this is doing is, it returns you the typescript type that is expected to have all necessary (required) keys and marks the rest, that will be filled in by the .default as optional.

So let's say you have

const Model = z.object({
    title: z.string(),
    active: z.boolean().default(false)
})

type ModelOutput = z.infer<typeof Model>
// ^ type ModelOutput = {
//    title: string;
//    active: boolean;
//}

type ModelInput = z.input<typeof Model>
// ^ type ModelInput = {
//    title: string;
//    active?: boolean | undefined; // << DEFAULT
// }

A generic Type-Safe method to instantiate a Zod Object with default values


const makeInstantiator =
  <T extends z.ZodType<any>>(model: T) =>
  (input: z.input<T>): z.output<T> => {
    return model.parse(input);
  };

const instantiateModel = makeInstantiator(Model);
// const instantiateModel: (input: {
//   title: string;
//   active?: boolean | undefined;
// }) => {
//   title: string;
//   active: boolean;
// }
Demy answered 9/10, 2023 at 10:52 Comment(0)
S
2

Note: I tried to use the accepted answer (.parse({})), but I often wanted my defaults to be values that wouldn't pass validation (making that method throw errors), and I also was having trouble with nested schema fields.


I was also trying to do this, and came up with the following solution, similar to an earlier answer. Some of the typing isn't the best (usually in the (fieldSchema._def as ...) areas), but it got rid of my linting errors.

It hasn't been tested thoroughly yet, but the following so far does a good job at creating a "default" object of a given Zod schema, with the following precedence (recursively handling any nested schema fields (z.object(..)):

  • if default value provided for field (z..default(..)), immediately return that
  • if no default value & is a "base" Zod schema field (e.g. z.number() z.string(), etc.), return a "base" default (I tucked mine away in a BASE_DEFAULTS object)
  • if no default value & is a "transformed" Zod schema field (e.g. z.refine(..), z.coerce(..), etc.), then try to take the "inner type" (z.ZodTransformationType<z.ZodInnerTypeIsLocatedHere?, ...>)
    • honestly, never really tested this one -- I just decided that I was going to make it standard practice to always provide a default, then I don't have to deal with transformed types

type ExtractedDefaults<T> = {
  [P in keyof T]?: T[P] extends ZodTypeAny ? ReturnType<T[P]["parse"]> : never;
};

export function extractDefaults<TSchema extends ZodRawShape>(
  schema: z.ZodObject<TSchema>,
): ExtractedDefaults<TSchema> {
  const schemaShape = schema.shape;
  const result = {} as ExtractedDefaults<TSchema>;

  for (const key in schemaShape) {
    const fieldSchema = schemaShape[key];
    result[key as keyof TSchema] = extractValueFromSchema(
      fieldSchema!,
    ) as ExtractedDefaults<TSchema>[keyof TSchema];
  }

  return result;
}

function extractValueFromSchema<T extends ZodTypeAny>(fieldSchema: T): unknown {
  if (fieldSchema instanceof z.ZodDefault) {
    return (
      fieldSchema._def as { defaultValue: () => unknown }
    ).defaultValue() as ReturnType<T["parse"]>;
  } else if (fieldSchema instanceof z.ZodObject) {
    return extractDefaultsForm(fieldSchema);
  } else if (fieldSchema instanceof z.ZodArray) {
    return BASE_DEFAULTS.ARRAY.slice();
  } else {
    return handleBaseTypes(fieldSchema);
  }
}

function handleBaseTypes<T extends ZodTypeAny>(fieldSchema: T): unknown {
  switch (fieldSchema.constructor) {
    case z.ZodString:
      return BASE_DEFAULTS.STRING;
    case z.ZodDate:
      return BASE_DEFAULTS.STRING;
    case z.ZodNumber:
      return BASE_DEFAULTS.NUMBER;
    case z.ZodBoolean:
      return BASE_DEFAULTS.BOOLEAN;
    case z.ZodNull:
      return BASE_DEFAULTS.NULL;
    case z.ZodNullable:
      return BASE_DEFAULTS.NULL;
    case z.ZodOptional:
      return BASE_DEFAULTS.UNDEFINED; // Choose appropriately between UNDEFINED or NULL
    default:
      return handleTransformedTypes(fieldSchema);
  }
}

function handleTransformedTypes<T extends ZodTypeAny>(fieldSchema: T): unknown {
  if (
    fieldSchema instanceof z.ZodTransformer &&
    (fieldSchema._def as { innerType: ZodTypeAny }).innerType
  ) {
    return extractValueFromSchema(
      (fieldSchema._def as { innerType: ZodTypeAny }).innerType,
    );
  }
  return BASE_DEFAULTS.UNDEFINED;
}

Note: so far, I'm only using this to create default values to use in react-hook-form. I actually use the BASE_DEFAULTS_FORM below rather than what you'd generally expect some of the defaults to be (since RHF doesn't like undefined, I tend to convert numbers to strings while inside forms to make them easier to work with, etc.).

export const BASE_DEFAULTS = {
  STRING: "",
  NUMBER: 0,
  BOOLEAN: false,
  DATE: getTodayPlusTime(), // today date object
  OBJECT: {}, // consider if this is best...
  ARRAY: [],
  NULL: null,
  UNDEFINED: undefined,
};

const BASE_DEFAULTS_FORM = {
  STRING: "",
  NUMBER: null,
  BOOLEAN: false,
  DATE: getTodayPlusTimeDateString(), // today string (specific format)
  OBJECT: {}, // consider if this is best...
  ARRAY: [],
  NULL: null,
  UNDEFINED: null, // consider if this is best...
};

Also, because my typing isn't perfect, I have to typecast where I use it. If anyone can improve the typing, please let me know!

export const schemaCreate = z.object(...)
export const TDocCreate = z.infer(typeof schemaCreate)

export const DEFAULT_VALUES = extractDefaults(
  schemaCreate,
) as unknown as TDocCreate;
Steinbach answered 14/4, 2024 at 0:45 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.