colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
33.28k stars 1.15k forks source link

Add method to remove defaults #1593

Open rjmackay opened 1 year ago

rjmackay commented 1 year ago

It would be helpful to have a method that removes all defaults from an object. Similar to .partial() or required().

My use case is that I have a type for validating API input for both create and update. On update, I want to accept partial input but not have the defaults applied again. I can construct this type manually, but it's painful on a large schema.

maxArturo commented 1 year ago

Seems a reasonable ask! Also shouldn't be hard to do a PR. In the meanwhile I wonder how you go about doing this manually? Naively, if I had to do this I'd invert the problem and it may simplify things quite a bit. You may already be doing this:

However I can see if you have deeply nested objects or some convoluted use case this could be painful :) .

santosmarco-caribou commented 1 year ago

Given the popularity of this type of request, I've opened a PR that implements ZodRequired. It removes the undefined from the inference and throws an error if the input is undefined, meaning that your defaults will never get called (since defaults only get called on undefined inputs).

https://github.com/colinhacks/zod/pull/1738

helmturner commented 1 year ago

I'm experimenting with a utility class that generates convenience methods for variations on a schema, and I've found the easiest path is to define defaults on the schema and recursively unwrap them via ZodDefault["removeDefault"] (implementation below).

Currently, this implementation only supports passing a ZodObject as the base schema because it was designed to work with tRPC, where it's more idiomatic to pass ZodObject schemas as an input parser. With some work, though, this could be made to work with any base schema, I believe.

function unwrapDefaultsFromSchemas<
  TRawShape extends z.ZodRawShape,
  TUnknownKeys extends z.UnknownKeysParam,
  TCatchall extends z.ZodTypeAny,
  TOutput extends z.objectOutputType<TRawShape, TCatchall>,
  TInput extends z.objectInputType<TRawShape, TCatchall>,
  TSchema extends z.ZodObject<
    TRawShape,
    TUnknownKeys,
    TCatchall,
    TOutput,
    TInput
  >
>(schema: TSchema): UnwrapDefaultsRecursive<TSchema> {
  return z.object(
    Object.entries(schema.shape).reduce((acc, [key, value]) => {
      let base = value;
      if (base instanceof z.ZodDefault) base = base.removeDefault();
      if (base instanceof z.ZodObject) base = unwrapDefaultsFromSchemas(base);
      acc[key] = base;
      return acc;
    }, {} as z.ZodRawShape)
  ) as any;
}

type UnwrapDefaultsRecursive<TSchema extends z.AnyZodObject> =
  TSchema extends z.ZodObject<
    infer Shape extends z.ZodRawShape,
    infer UnknownKeys extends z.UnknownKeysParam,
    infer Catchall extends z.ZodTypeAny
  >
    ? z.ZodObject<
        {
          [K in keyof Shape]: Shape[K] extends z.ZodDefault<any>
            ? ReturnType<Shape[K]["removeDefault"]>
            : Shape[K] extends z.AnyZodObject
            ? UnwrapDefaultsRecursive<Shape[K]>
            : Shape[K];
        },
        UnknownKeys,
        Catchall
      >
    : never;