colinhacks / zod

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

Nested schema w/ .default doesn't generate the default value if it is used with .optional #3054

Open robogeek opened 10 months ago

robogeek commented 10 months ago

Consider these three schema objects -- all generated by ts-to-zod from TypeScript source.

export const dateTimeSchema = z.string().datetime().default("0000-00-00");

export const durationSchema = z
  .string()
  .regex(
    /^(-?)P(?=\d|T\d)(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)([DW]))?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/,
  )
  .default("PT0S");

export const intervalPeriodSchema = z.object({
  start: dateTimeSchema,
  duration: durationSchema.optional(),
  randomizeStart: durationSchema.optional(),
});

In durationSchema there is a default value, which is a Duration string for zero seconds.

Calling intervalPeriodSchema.parse with an object that does not include the randomizeStart field, I get the following:

{
  // ...
  intervalPeriod: { start: '2023-02-20T00:00:00Z', duration: 'P3M' },
  // ...
}

What I'm reasoning is that intervalPeriodSchema.parse calls durationSchema.parse which notices the missing field, notices the .optional, and therefore does not call into durationSchema.parse, and therefore the .default value is not substituted.

I have generated the same schema using a different tool - openapi-to-zod - which generates the following schema, and in that case the default value shows up.

    intervalPeriod: z
      .object({
        start: z.string().datetime().describe("datetime in ISO 8601 format"),
        duration: z
          .string()
          .regex(
            new RegExp(
              "^(-?)P(?=\\d|T\\d)(?:(\\d+)Y)?(?:(\\d+)M)?(?:(\\d+)([DW]))?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+(?:\\.\\d+)?)S)?)?$"
            )
          )
          .describe("duration in ISO 8601 format")
          .default("PT0S"),
        randomizeStart: z
          .string()
          .regex(
            new RegExp(
              "^(-?)P(?=\\d|T\\d)(?:(\\d+)Y)?(?:(\\d+)M)?(?:(\\d+)([DW]))?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+(?:\\.\\d+)?)S)?)?$"
            )
          )
          .describe("duration in ISO 8601 format")
          .default("PT0S"),
      })
INeedJobToStartWork commented 9 months ago

I think i had same problem. You CANT use .optional() with .default() because it not gonna generate them + default .default() make them optional.

To not get error which tell you about needed input which is default generated - so optional

use z.input<> instead z.infer<>. z.infer it's more like final form when z.input its correctness before parsing.

I hope it gonna help.