zod icon indicating copy to clipboard operation
zod copied to clipboard

Use default value on error

Open matthewoates opened this issue 2 years ago • 8 comments

I'd like to validate a value, and if that validation fails I don't care about the error - I want to use a default value.

const strZ = z.string().default('hello world');
strZ.parse() // 'hello world'
strZ.parse('hi') // 'hi'
strZ.parse(23) // error, but I want 'hello world'

I can't figure out a clean way to do this without writing my own wrapper around zod. Is this supported already by the library?

matthewoates avatar Sep 25 '22 15:09 matthewoates

I am running into the same issue. Mine is an object schema like this:

export const urlQueryParams = z.object({
  queryParam1: z.preprocess(
    (value) => normalizeIntegerPriceAmount(value),
    z.number().positive().max(1000),
  ), // good - has transformation
  queryParam2: z.string(),
  queryParam3: z.string(),
  queryParam4: z.string(),
  queryParam5: z.string(),
  queryParam6: z.string(),
  queryParam7: z.string().nullish().refine(isUUID).default(null),
});

urlQueryParams.parse({
  queryParam1: '10032', // transformed to 100.32 (decimal number)
  queryParam2: 'good',
  queryParam3: 'good',
  queryParam4: 'good',
  queryParam5: 'good',
  queryParam6: 'good',
  queryParam7: '0000', // not a valid UUID, but the default `null` will do
});

Because queryParam7 is not a UUID, the whole validation fails, even though a default value could have been returned. In addition, any transformations that are done on the rest of the properties that have passed the validation are lost.

My current best idea is to cycle through the schema.shape keys (subschemas) and run them individually, so that I also get the transformed values for the "good" fields. Perhaps you can recommend a better approach.

Thank you, Zod Authors, for this amazing library!

zavarka avatar Sep 27 '22 20:09 zavarka

Throwing in some ideas here:

type Parser<T> = (val: unknown) => T;
const customSchema = <T>(parser: Parser<T>) => z.unknown().transform(parser);

// Now you can define your own zod schema like this:
const catOrDogSchema = customSchema((val) => {
  if (val === "cat" || val === "dog") return val;
  throw new Error("Not cat or dog");
});
catOrDogSchema.parse("cat") // type: "cat" | "dog"

We can extend this and make withFallback helper:

const withFallback = <T>(schema: z.ZodType<T>, fallback: T) =>
  customSchema((val) => {
    const parsed = schema.safeParse(val);
    if (parsed.success) return parsed.data;
    return fallback;
  });

@matthewoates

const strZ = withFallback(z.string().default("hello world"), "fallback");
strZ.parse() // "hello world"
strZ.parse("hi") // "hi"
strZ.parse(23) // "fallback"

@zavarka

const urlQueryParams = z.object({
  // ...
  queryParam7: withFallback(
    z.string().nullish().refine(isUUID),
    null
  ),
});
urlQueryParams.parse({
  // ...
  queryParam7: 13 // INVALID
}); // but it parses successfully. { queryParam7: null }

This works, but it would be nice if zod provided corresponding methods like z.custom() and z.string().fallback().

0916dhkim avatar Sep 28 '22 03:09 0916dhkim

I think fallback would be a nice feature for sure! FWIW, Zod actually does have a z.custom schema that is just undocumented. It takes a (unknown) => boolean function.

scotttrinh avatar Sep 28 '22 16:09 scotttrinh

Another idea is we could leverage the pre-existing functionality of disable and have something like:

const strZ = z.string().ignoreErrors().default('hello world');
strZ.parse() // 'hello world'
strZ.parse('hi') // 'hi'
strZ.parse(23) // 'hello world'

I think something like default and fallback could be confusing - especially since the functionality is similar, but my suggestion probably breaks other semantics.

Maybe this could be an option to default, assuming default() is still invoked if there's an error earlier in the function chain.

const strZ = z.string().default('hello world', { ignoreErrors: true });
strZ.parse() // 'hello world'
strZ.parse('hi') // 'hi'
strZ.parse(23) // 'hello world'

My use case is trying to parse and validate query strings:

'?d=1-3&c=2-4'

// into:
{
  difficulty: [1, 3],
  complexity: [2, 4]
}

matthewoates avatar Sep 29 '22 17:09 matthewoates

Looks like this might be a good workaround:

const withFallback = <T>(schema: z.ZodType<T>, fallback: T) =>
  z.preprocess(
    (value) => {
      const parseResult = schema.safeParse(value);
      if (parseResult.success) return value;
      return fallback;
    },
    z.custom((v) => true)
  );

Example Usage

altitudems avatar Sep 29 '22 19:09 altitudems

Indeed, withFallback is working exactly as desired:

const urlQueryParams = z.object({
  queryParam1: z.preprocess(
    (value) => Number(value) / 100,
    z.number().positive().max(1000)
  ), // good - has transformation
  queryParam2: z.string(),
  queryParam3: z.string(),
  queryParam4: z.string(),
  queryParam5: z.string(),
  queryParam6: z.string(),
  queryParam7: withFallback(z.string().nullish().refine(isUUID), null),
});

Example Usage - based on @altitudems code sample

Thank you!

zavarka avatar Sep 29 '22 21:09 zavarka

@scotttrinh, do you know what is needed to incorporate the suggested withFallback API into the library, so that we don't need to replicate the definition of it in every project?

zavarka avatar Sep 29 '22 21:09 zavarka

I think having both fallback and default is very confusing. Also, default adds ? to infered input type afaik, which might not be desirable if you're defining a function's input and want your consumer to pass that field.

Maybe .catch() that accepts either a fallback value or a callback that takes a faulty value (or ZodIssue?) and returns fallback value?

const urlQueryParams = z.object({
  queryParam7: z.string().nullish().refine(isUUID).catch(null), // string | null
  queryParam8: z.number().integer().catch((faultyValue) => /* conditionally return some values or rethrow */)
});

igalklebanov avatar Nov 26 '22 15:11 igalklebanov

Has this issue been resolved? If so, I would like to close this issue.

JacobWeisenburger avatar Jan 05 '23 02:01 JacobWeisenburger

Yes, I think this is resolved by the catch method.

0916dhkim avatar Jan 05 '23 16:01 0916dhkim

catch is exactly what I had in mind - brilliant naming and API. 👏

matthewoates avatar Jan 15 '23 21:01 matthewoates

Is there anyway to get the data if safeparse fails (for example any other field that suceeded)?

ddeisadze avatar Mar 23 '24 23:03 ddeisadze