zod icon indicating copy to clipboard operation
zod copied to clipboard

Add method to remove defaults

Open rjmackay opened this issue 2 years ago • 2 comments

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.

rjmackay avatar Nov 24 '22 04:11 rjmackay

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:

  • Start out with your base schema, no defaults nor partials
  • make a createSchema, via an object of type Record<string, z.ZodType>:
const test: Record<string, z.ZodType> = {
    id: z.string().min(3),
    user: z.string(),
    //...
    createdAt: z.date(),
  };

  const defaults = {
    id: "111",
    user: "bob",
    createdAt: () => new Date(),
  };

  const createSchema = z.object(
    Object.entries(test).reduce<Record<string, z.ZodType>>(
      (prev, [key, val]) => {
        if (defaults[key]) {
          prev[key] = val.default(defaults[key]);
        } else {
          prev[key] = val;
        }
        return prev;
      },
      {}
    )
  );

  const updateSchema = z.object(test).partial();

  createSchema.parse({});
  updateSchema.parse({ id: "111" });

  console.log("success");
}

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

maxArturo avatar Nov 24 '22 13:11 maxArturo

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

santosmarco-caribou avatar Dec 22 '22 07:12 santosmarco-caribou

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;

helmturner avatar Jan 31 '23 19:01 helmturner