Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1227 Feature default on mismatch #1537

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 32 additions & 1 deletion README.md
Expand Up @@ -100,6 +100,7 @@
- [.superRefine](#superRefine)
- [.transform](#transform)
- [.default](#default)
- [.catch](#catch)
- [.optional](#optional)
- [.nullable](#nullable)
- [.nullish](#nullish)
Expand Down Expand Up @@ -948,7 +949,6 @@ const deepPartialUser = user.deepPartial();

> Important limitation: deep partials only work as expected in hierarchies of objects, arrays, and tuples.


### `.required`

Contrary to the `.partial` method, the `.required` method makes all properties required.
Expand Down Expand Up @@ -1872,6 +1872,37 @@ numberWithRandomDefault.parse(undefined); // => 0.1871840107401901
numberWithRandomDefault.parse(undefined); // => 0.7223408162401552
```

Conceptually, this is how Zod processes default values:

1. If the input is `undefined`, the default value is returned
2. Otherwise, the data is parsed using the base schema

### `.catch`

Use `.catch()` to provide a "catch value" to be returned in the event of a parsing error.

```ts
const numberWithCatch = z.number().catch(42);

numberWithCatch.parse(5); // => 5
numberWithCatch.parse("tuna"); // => 42
```

Optionally, you can pass a function into `.catch` that will be re-executed whenever a default value needs to be generated:

```ts
const numberWithRandomCatch = z.number().catch(Math.random);

numberWithRandomDefault.parse("sup"); // => 0.4413456736055323
numberWithRandomDefault.parse("sup"); // => 0.1871840107401901
numberWithRandomDefault.parse("sup"); // => 0.7223408162401552
```

Conceptually, this is how Zod processes "catch values":

1. The data is parsed using the base schema
2. If the parsing fails, the "catch value" is returned

### `.optional`

A convenience method that returns an optional version of a schema.
Expand Down
39 changes: 35 additions & 4 deletions deno/lib/README.md
Expand Up @@ -100,6 +100,7 @@
- [.superRefine](#superRefine)
- [.transform](#transform)
- [.default](#default)
- [.catch](#catch)
- [.optional](#optional)
- [.nullable](#nullable)
- [.nullish](#nullish)
Expand Down Expand Up @@ -779,7 +780,7 @@ nullableString.parse(null); // => null
Or use the `.nullable()` method.

```ts
const E = z.string().nullable(); // equivalent to D
const E = z.string().nullable(); // equivalent to nullableString
type E = z.infer<typeof E>; // string | null
```

Expand Down Expand Up @@ -948,7 +949,6 @@ const deepPartialUser = user.deepPartial();

> Important limitation: deep partials only work as expected in hierarchies of objects, arrays, and tuples.


### `.required`

Contrary to the `.partial` method, the `.required` method makes all properties required.
Expand Down Expand Up @@ -1872,6 +1872,37 @@ numberWithRandomDefault.parse(undefined); // => 0.1871840107401901
numberWithRandomDefault.parse(undefined); // => 0.7223408162401552
```

Conceptually, this is how Zod processes default values:

1. If the input is `undefined`, the default value is returned
2. Otherwise, the data is parsed using the base schema

### `.catch`

Use `.catch()` to provide a "catch value" to be returned in the event of a parsing error.

```ts
const numberWithCatch = z.number().catch(42);

numberWithCatch.parse(5); // => 5
numberWithCatch.parse("tuna"); // => 42
```

Optionally, you can pass a function into `.catch` that will be re-executed whenever a default value needs to be generated:

```ts
const numberWithRandomCatch = z.number().catch(Math.random);

numberWithRandomDefault.parse("sup"); // => 0.4413456736055323
numberWithRandomDefault.parse("sup"); // => 0.1871840107401901
numberWithRandomDefault.parse("sup"); // => 0.7223408162401552
```

Conceptually, this is how Zod processes "catch values":

1. The data is parsed using the base schema
2. If the parsing fails, the "catch value" is returned

### `.optional`

A convenience method that returns an optional version of a schema.
Expand Down Expand Up @@ -1967,7 +1998,7 @@ petCat(fido); // works fine
In some cases, its can be desirable to simulate _nominal typing_ inside TypeScript. For instance, you may wish to write a function that only accepts an input that has been validated by Zod. This can be achieved with _branded types_ (AKA _opaque types_).

```ts
const Cat = z.object({ name: z.string }).brand<"Cat">();
const Cat = z.object({ name: z.string() }).brand<"Cat">();
type Cat = z.infer<typeof Cat>;

const petCat = (cat: Cat) => {};
Expand All @@ -1983,7 +2014,7 @@ petCat({ name: "fido" });
Under the hood, this works by attaching a "brand" to the inferred type using an intersection type. This way, plain/unbranded data structures are no longer assignable to the inferred type of the schema.

```ts
const Cat = z.object({ name: z.string }).brand<"Cat">();
const Cat = z.object({ name: z.string() }).brand<"Cat">();
type Cat = z.infer<typeof Cat>;
// {name: string} & {[symbol]: "Cat"}
```
Expand Down
144 changes: 144 additions & 0 deletions deno/lib/__tests__/catch.test.ts
@@ -0,0 +1,144 @@
// @ts-ignore TS6133
import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts";
const test = Deno.test;

import { z } from "../index.ts";
import { util } from "../helpers/util.ts";

test("basic catch", () => {
expect(z.string().catch("default").parse(undefined)).toBe("default");
});

test("basic catch async", async () => {
const result = await z.string().catch("default").parseAsync(1243);
expect(result).toBe("default");
});

test("catch replace wrong types", () => {
expect(z.string().catch("default").parse(true)).toBe("default");
expect(z.string().catch("default").parse(true)).toBe("default");
expect(z.string().catch("default").parse(15)).toBe("default");
expect(z.string().catch("default").parse([])).toBe("default");
expect(z.string().catch("default").parse(new Map())).toBe("default");
expect(z.string().catch("default").parse(new Set())).toBe("default");
expect(z.string().catch("default").parse({})).toBe("default");
});

test("catch with transform", () => {
const stringWithDefault = z
.string()
.transform((val) => val.toUpperCase())
.catch("default");
expect(stringWithDefault.parse(undefined)).toBe("default");
expect(stringWithDefault.parse(15)).toBe("default");
expect(stringWithDefault).toBeInstanceOf(z.ZodCatch);
expect(stringWithDefault._def.innerType).toBeInstanceOf(z.ZodEffects);
expect(stringWithDefault._def.innerType._def.schema).toBeInstanceOf(
z.ZodSchema
);

type inp = z.input<typeof stringWithDefault>;
util.assertEqual<inp, string | undefined>(true);
type out = z.output<typeof stringWithDefault>;
util.assertEqual<out, string>(true);
});

test("catch on existing optional", () => {
const stringWithDefault = z.string().optional().catch("asdf");
expect(stringWithDefault.parse(undefined)).toBe(undefined);
expect(stringWithDefault.parse(15)).toBe("asdf");
expect(stringWithDefault).toBeInstanceOf(z.ZodCatch);
expect(stringWithDefault._def.innerType).toBeInstanceOf(z.ZodOptional);
expect(stringWithDefault._def.innerType._def.innerType).toBeInstanceOf(
z.ZodString
);

type inp = z.input<typeof stringWithDefault>;
util.assertEqual<inp, string | undefined>(true);
type out = z.output<typeof stringWithDefault>;
util.assertEqual<out, string>(true);
});

test("optional on catch", () => {
const stringWithDefault = z.string().catch("asdf").optional();

type inp = z.input<typeof stringWithDefault>;
util.assertEqual<inp, string | undefined>(true);
type out = z.output<typeof stringWithDefault>;
util.assertEqual<out, string | undefined>(true);
});

test("complex chain example", () => {
const complex = z
.string()
.catch("asdf")
.transform((val) => val + "!")
.transform((val) => val.toUpperCase())
.catch("qwer")
.removeDefault()
.optional()
.catch("asdfasdf");

expect(complex.parse("qwer")).toBe("QWER!");
expect(complex.parse(15)).toBe("ASDF!");
expect(complex.parse(true)).toBe("ASDF!");
});

test("removeDefault", () => {
const stringWithRemovedDefault = z.string().catch("asdf").removeDefault();

type out = z.output<typeof stringWithRemovedDefault>;
util.assertEqual<out, string>(true);
});

test("nested", () => {
const inner = z.string().catch("asdf");
const outer = z.object({ inner }).catch({
inner: "asdf",
});
type input = z.input<typeof outer>;
util.assertEqual<input, { inner?: string | undefined } | undefined>(true);
type out = z.output<typeof outer>;
util.assertEqual<out, { inner: string }>(true);
expect(outer.parse(undefined)).toEqual({ inner: "asdf" });
expect(outer.parse({})).toEqual({ inner: "asdf" });
expect(outer.parse({ inner: undefined })).toEqual({ inner: "asdf" });
});

test("chained catch", () => {
const stringWithDefault = z.string().catch("inner").catch("outer");
const result = stringWithDefault.parse(undefined);
expect(result).toEqual("inner");
const resultDiff = stringWithDefault.parse(5);
expect(resultDiff).toEqual("inner");
});

test("factory", () => {
z.ZodCatch.create(z.string(), {
default: "asdf",
}).parse(undefined);
});

test("native enum", () => {
enum Fruits {
apple = "apple",
orange = "orange",
}

const schema = z.object({
fruit: z.nativeEnum(Fruits).catch(Fruits.apple),
});

expect(schema.parse({})).toEqual({ fruit: Fruits.apple });
expect(schema.parse({ fruit: 15 })).toEqual({ fruit: Fruits.apple });
});

test("enum", () => {
const schema = z.object({
fruit: z.enum(["apple", "orange"]).catch("apple"),
});

expect(schema.parse({})).toEqual({ fruit: "apple" });
expect(schema.parse({ fruit: true })).toEqual({ fruit: "apple" });
expect(schema.parse({ fruit: 15 })).toEqual({ fruit: "apple" });
});
4 changes: 3 additions & 1 deletion deno/lib/__tests__/default.test.ts
Expand Up @@ -92,7 +92,9 @@ test("chained defaults", () => {
});

test("factory", () => {
z.ZodDefault.create(z.string()).parse(undefined);
expect(
z.ZodDefault.create(z.string(), { default: "asdf" }).parse(undefined)
).toEqual("asdf");
});

test("native enum", () => {
Expand Down
2 changes: 2 additions & 0 deletions deno/lib/__tests__/firstparty.test.ts
Expand Up @@ -69,6 +69,8 @@ test("first party switch", () => {
break;
case z.ZodFirstPartyTypeKind.ZodDefault:
break;
case z.ZodFirstPartyTypeKind.ZodCatch:
break;
case z.ZodFirstPartyTypeKind.ZodPromise:
break;
case z.ZodFirstPartyTypeKind.ZodBranded:
Expand Down