From c2108de2455b6c8cdb9bff0ed241687c33bd4c8b Mon Sep 17 00:00:00 2001 From: seancrowe Date: Sat, 25 Jun 2022 19:50:42 -0500 Subject: [PATCH 1/7] Add ZodDefaultOnMismatch A new class ZodDefaultOnMismatch which will replace the data with the default value when the provided value is a mismatch in type with the expected value. When data is undefined, ZodDefaultOnMismatch acts like ZodDefault. --- deno/lib/__tests__/defaultOnMismatch.test.ts | 149 +++++++++++++++++++ deno/lib/__tests__/firstparty.test.ts | 2 + deno/lib/types.ts | 78 +++++++++- src/__tests__/defaultOnMismatch.test.ts | 148 ++++++++++++++++++ src/__tests__/firstparty.test.ts | 2 + src/types.ts | 78 +++++++++- 6 files changed, 443 insertions(+), 14 deletions(-) create mode 100644 deno/lib/__tests__/defaultOnMismatch.test.ts create mode 100644 src/__tests__/defaultOnMismatch.test.ts diff --git a/deno/lib/__tests__/defaultOnMismatch.test.ts b/deno/lib/__tests__/defaultOnMismatch.test.ts new file mode 100644 index 000000000..3c08d5195 --- /dev/null +++ b/deno/lib/__tests__/defaultOnMismatch.test.ts @@ -0,0 +1,149 @@ +// @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 defaultOnMismatch", () => { + expect(z.string().defaultOnMismatch("default").parse(undefined)).toBe("default"); +}); + +test("defaultOnMismatch replace wrong types", () => { + expect(z.string().defaultOnMismatch("default").parse(true)).toBe("default"); + expect(z.string().defaultOnMismatch("default").parse(true)).toBe("default"); + expect(z.string().defaultOnMismatch("default").parse(15)).toBe("default"); + expect(z.string().defaultOnMismatch("default").parse([])).toBe("default"); + expect(z.string().defaultOnMismatch("default").parse(new Map())).toBe("default"); + expect(z.string().defaultOnMismatch("default").parse(new Set())).toBe("default"); + expect(z.string().defaultOnMismatch("default").parse({})).toBe("default"); +}); + +test("defaultOnMismatch with transform", () => { + const stringWithDefault = z + .string() + .transform((val) => val.toUpperCase()) + .defaultOnMismatch("default"); + expect(stringWithDefault.parse(undefined)).toBe("DEFAULT"); + expect(stringWithDefault.parse(15)).toBe("DEFAULT"); + expect(stringWithDefault).toBeInstanceOf(z.ZodDefaultOnMismatch); + expect(stringWithDefault._def.innerType).toBeInstanceOf(z.ZodEffects); + expect(stringWithDefault._def.innerType._def.schema).toBeInstanceOf( + z.ZodSchema + ); + + type inp = z.input; + const f1: util.AssertEqual = true; + type out = z.output; + const f2: util.AssertEqual = true; + f1; + f2; +}); + +test("defaultOnMismatch on existing optional", () => { + const stringWithDefault = z.string().optional().defaultOnMismatch("asdf"); + expect(stringWithDefault.parse(undefined)).toBe("asdf"); + expect(stringWithDefault.parse(15)).toBe("asdf"); + expect(stringWithDefault).toBeInstanceOf(z.ZodDefaultOnMismatch); + expect(stringWithDefault._def.innerType).toBeInstanceOf(z.ZodOptional); + expect(stringWithDefault._def.innerType._def.innerType).toBeInstanceOf( + z.ZodString + ); + + type inp = z.input; + const f1: util.AssertEqual = true; + type out = z.output; + const f2: util.AssertEqual = true; + f1; + f2; +}); + +test("optional on defaultOnMismatch", () => { + const stringWithDefault = z.string().defaultOnMismatch("asdf").optional(); + + type inp = z.input; + const f1: util.AssertEqual = true; + type out = z.output; + const f2: util.AssertEqual = true; + f1; + f2; +}); + +test("complex chain example", () => { + const complex = z + .string() + .defaultOnMismatch("asdf") + .transform((val) => val + "!") + .transform((val) => val.toUpperCase()) + .defaultOnMismatch("qwer") + .removeDefault() + .optional() + .defaultOnMismatch("asdfasdf"); + + expect(complex.parse(undefined)).toBe("ASDFASDF!"); + expect(complex.parse(15)).toBe("ASDFASDF!"); + expect(complex.parse(true)).toBe("ASDFASDF!"); +}); + +test("removeDefault", () => { + const stringWithRemovedDefault = z.string().defaultOnMismatch("asdf").removeDefault(); + + type out = z.output; + const f2: util.AssertEqual = true; + f2; +}); + +test("nested", () => { + const inner = z.string().defaultOnMismatch("asdf"); + const outer = z.object({ inner }).defaultOnMismatch({ + inner: "asdf", + }); + type input = z.input; + const f1: util.AssertEqual< + input, + { inner?: string | undefined } | undefined + > = true; + type out = z.output; + const f2: util.AssertEqual = true; + f1; + f2; + expect(outer.parse(undefined)).toEqual({ inner: "asdf" }); + expect(outer.parse({})).toEqual({ inner: "asdf" }); + expect(outer.parse({ inner: undefined })).toEqual({ inner: "asdf" }); +}); + +test("chained defaultOnMismatch", () => { + const stringWithDefault = z.string().defaultOnMismatch("inner").defaultOnMismatch("outer"); + const result = stringWithDefault.parse(undefined); + expect(result).toEqual("outer"); + const resultDiff = stringWithDefault.parse(5); + expect(resultDiff).toEqual("outer"); +}); + +test("factory", () => { + z.ZodDefaultOnMismatch.create(z.string()).parse(undefined); +}); + +test("native enum", () => { + enum Fruits { + apple = "apple", + orange = "orange", + } + + const schema = z.object({ + fruit: z.nativeEnum(Fruits).defaultOnMismatch(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"]).defaultOnMismatch("apple"), + }); + + expect(schema.parse({})).toEqual({ fruit: "apple" }); + expect(schema.parse({fruit:true})).toEqual({ fruit: "apple" }); + expect(schema.parse({fruit:15})).toEqual({ fruit: "apple" }); +}); diff --git a/deno/lib/__tests__/firstparty.test.ts b/deno/lib/__tests__/firstparty.test.ts index 39321571f..e44047adb 100644 --- a/deno/lib/__tests__/firstparty.test.ts +++ b/deno/lib/__tests__/firstparty.test.ts @@ -69,6 +69,8 @@ test("first party switch", () => { break; case z.ZodFirstPartyTypeKind.ZodDefault: break; + case z.ZodFirstPartyTypeKind.ZodDefaultOnMismatch: + break; case z.ZodFirstPartyTypeKind.ZodPromise: break; case z.ZodFirstPartyTypeKind.ZodBranded: diff --git a/deno/lib/types.ts b/deno/lib/types.ts index b3754b657..f049aa184 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -369,6 +369,7 @@ export abstract class ZodType< this.transform = this.transform.bind(this); this.brand = this.brand.bind(this); this.default = this.default.bind(this); + this.defaultOnMismatch = this.defaultOnMismatch.bind(this); this.describe = this.describe.bind(this); this.isNullable = this.isNullable.bind(this); this.isOptional = this.isOptional.bind(this); @@ -427,6 +428,19 @@ export abstract class ZodType< ...processCreateParams(undefined), }); } + defaultOnMismatch(def: util.noUndefined): ZodDefaultOnMismatch; + defaultOnMismatch( + def: () => util.noUndefined + ): ZodDefaultOnMismatch; + defaultOnMismatch(def: any) { + const defaultValueFunc = typeof def === "function" ? def : () => def; + + return new ZodDefaultOnMismatch({ + innerType: this, + defaultValue: defaultValueFunc, + typeName: ZodFirstPartyTypeKind.ZodDefaultOnMismatch, + }) as any; + } describe(description: string): this { const This = (this as any).constructor; @@ -3739,13 +3753,61 @@ export class ZodDefault extends ZodType< }; } -////////////////////////////////////// -////////////////////////////////////// -////////// ////////// -////////// ZodNaN ////////// -////////// ////////// -////////////////////////////////////// -////////////////////////////////////// +//////////////////////////////////////////// +//////////////////////////////////////////// +////////// ////////// +////////// ZodDefaultOnMismatch ////////// +////////// ////////// +//////////////////////////////////////////// +//////////////////////////////////////////// +export interface ZodDefaultOnMismatchDef + extends ZodTypeDef { + innerType: T; + defaultValue: () => util.noUndefined; + typeName: ZodFirstPartyTypeKind.ZodDefaultOnMismatch; +} + +export class ZodDefaultOnMismatch extends ZodType< + util.noUndefined, + ZodDefaultOnMismatchDef, + T["_input"] | undefined +> { + _parse(input: ParseInput): ParseReturnType { + const { ctx } = this._processInputParams(input); + const defaultValue = this._def.defaultValue(); + return this._def.innerType._parse({ + data: + ctx.parsedType !== getParsedType(defaultValue) + ? defaultValue + : ctx.data, + path: ctx.path, + parent: ctx, + }); + } + + removeDefault() { + return this._def.innerType; + } + + static create = ( + type: T, + params?: RawCreateParams + ): ZodOptional => { + return new ZodOptional({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodOptional, + ...processCreateParams(params), + }) as any; + }; +} + +///////////////////////////////////////// +///////////////////////////////////////// +////////// ////////// +////////// ZodNaN ////////// +////////// ////////// +///////////////////////////////////////// +///////////////////////////////////////// export interface ZodNaNDef extends ZodTypeDef { typeName: ZodFirstPartyTypeKind.ZodNaN; @@ -3865,6 +3927,7 @@ export enum ZodFirstPartyTypeKind { ZodOptional = "ZodOptional", ZodNullable = "ZodNullable", ZodDefault = "ZodDefault", + ZodDefaultOnMismatch = "ZodDefaultOnMismatch", ZodPromise = "ZodPromise", ZodBranded = "ZodBranded", } @@ -3899,6 +3962,7 @@ export type ZodFirstPartySchemaTypes = | ZodOptional | ZodNullable | ZodDefault + | ZodDefaultOnMismatch | ZodPromise | ZodBranded; diff --git a/src/__tests__/defaultOnMismatch.test.ts b/src/__tests__/defaultOnMismatch.test.ts new file mode 100644 index 000000000..f7225ec07 --- /dev/null +++ b/src/__tests__/defaultOnMismatch.test.ts @@ -0,0 +1,148 @@ +// @ts-ignore TS6133 +import { expect, test } from "@jest/globals"; + +import { z } from ".."; +import { util } from "../helpers/util"; + +test("basic defaultOnMismatch", () => { + expect(z.string().defaultOnMismatch("default").parse(undefined)).toBe("default"); +}); + +test("defaultOnMismatch replace wrong types", () => { + expect(z.string().defaultOnMismatch("default").parse(true)).toBe("default"); + expect(z.string().defaultOnMismatch("default").parse(true)).toBe("default"); + expect(z.string().defaultOnMismatch("default").parse(15)).toBe("default"); + expect(z.string().defaultOnMismatch("default").parse([])).toBe("default"); + expect(z.string().defaultOnMismatch("default").parse(new Map())).toBe("default"); + expect(z.string().defaultOnMismatch("default").parse(new Set())).toBe("default"); + expect(z.string().defaultOnMismatch("default").parse({})).toBe("default"); +}); + +test("defaultOnMismatch with transform", () => { + const stringWithDefault = z + .string() + .transform((val) => val.toUpperCase()) + .defaultOnMismatch("default"); + expect(stringWithDefault.parse(undefined)).toBe("DEFAULT"); + expect(stringWithDefault.parse(15)).toBe("DEFAULT"); + expect(stringWithDefault).toBeInstanceOf(z.ZodDefaultOnMismatch); + expect(stringWithDefault._def.innerType).toBeInstanceOf(z.ZodEffects); + expect(stringWithDefault._def.innerType._def.schema).toBeInstanceOf( + z.ZodSchema + ); + + type inp = z.input; + const f1: util.AssertEqual = true; + type out = z.output; + const f2: util.AssertEqual = true; + f1; + f2; +}); + +test("defaultOnMismatch on existing optional", () => { + const stringWithDefault = z.string().optional().defaultOnMismatch("asdf"); + expect(stringWithDefault.parse(undefined)).toBe("asdf"); + expect(stringWithDefault.parse(15)).toBe("asdf"); + expect(stringWithDefault).toBeInstanceOf(z.ZodDefaultOnMismatch); + expect(stringWithDefault._def.innerType).toBeInstanceOf(z.ZodOptional); + expect(stringWithDefault._def.innerType._def.innerType).toBeInstanceOf( + z.ZodString + ); + + type inp = z.input; + const f1: util.AssertEqual = true; + type out = z.output; + const f2: util.AssertEqual = true; + f1; + f2; +}); + +test("optional on defaultOnMismatch", () => { + const stringWithDefault = z.string().defaultOnMismatch("asdf").optional(); + + type inp = z.input; + const f1: util.AssertEqual = true; + type out = z.output; + const f2: util.AssertEqual = true; + f1; + f2; +}); + +test("complex chain example", () => { + const complex = z + .string() + .defaultOnMismatch("asdf") + .transform((val) => val + "!") + .transform((val) => val.toUpperCase()) + .defaultOnMismatch("qwer") + .removeDefault() + .optional() + .defaultOnMismatch("asdfasdf"); + + expect(complex.parse(undefined)).toBe("ASDFASDF!"); + expect(complex.parse(15)).toBe("ASDFASDF!"); + expect(complex.parse(true)).toBe("ASDFASDF!"); +}); + +test("removeDefault", () => { + const stringWithRemovedDefault = z.string().defaultOnMismatch("asdf").removeDefault(); + + type out = z.output; + const f2: util.AssertEqual = true; + f2; +}); + +test("nested", () => { + const inner = z.string().defaultOnMismatch("asdf"); + const outer = z.object({ inner }).defaultOnMismatch({ + inner: "asdf", + }); + type input = z.input; + const f1: util.AssertEqual< + input, + { inner?: string | undefined } | undefined + > = true; + type out = z.output; + const f2: util.AssertEqual = true; + f1; + f2; + expect(outer.parse(undefined)).toEqual({ inner: "asdf" }); + expect(outer.parse({})).toEqual({ inner: "asdf" }); + expect(outer.parse({ inner: undefined })).toEqual({ inner: "asdf" }); +}); + +test("chained defaultOnMismatch", () => { + const stringWithDefault = z.string().defaultOnMismatch("inner").defaultOnMismatch("outer"); + const result = stringWithDefault.parse(undefined); + expect(result).toEqual("outer"); + const resultDiff = stringWithDefault.parse(5); + expect(resultDiff).toEqual("outer"); +}); + +test("factory", () => { + z.ZodDefaultOnMismatch.create(z.string()).parse(undefined); +}); + +test("native enum", () => { + enum Fruits { + apple = "apple", + orange = "orange", + } + + const schema = z.object({ + fruit: z.nativeEnum(Fruits).defaultOnMismatch(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"]).defaultOnMismatch("apple"), + }); + + expect(schema.parse({})).toEqual({ fruit: "apple" }); + expect(schema.parse({fruit:true})).toEqual({ fruit: "apple" }); + expect(schema.parse({fruit:15})).toEqual({ fruit: "apple" }); +}); diff --git a/src/__tests__/firstparty.test.ts b/src/__tests__/firstparty.test.ts index 34932de82..3fbac741a 100644 --- a/src/__tests__/firstparty.test.ts +++ b/src/__tests__/firstparty.test.ts @@ -68,6 +68,8 @@ test("first party switch", () => { break; case z.ZodFirstPartyTypeKind.ZodDefault: break; + case z.ZodFirstPartyTypeKind.ZodDefaultOnMismatch: + break; case z.ZodFirstPartyTypeKind.ZodPromise: break; case z.ZodFirstPartyTypeKind.ZodBranded: diff --git a/src/types.ts b/src/types.ts index 83883a23c..bf024f58f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -369,6 +369,7 @@ export abstract class ZodType< this.transform = this.transform.bind(this); this.brand = this.brand.bind(this); this.default = this.default.bind(this); + this.defaultOnMismatch = this.defaultOnMismatch.bind(this); this.describe = this.describe.bind(this); this.isNullable = this.isNullable.bind(this); this.isOptional = this.isOptional.bind(this); @@ -427,6 +428,19 @@ export abstract class ZodType< ...processCreateParams(undefined), }); } + defaultOnMismatch(def: util.noUndefined): ZodDefaultOnMismatch; + defaultOnMismatch( + def: () => util.noUndefined + ): ZodDefaultOnMismatch; + defaultOnMismatch(def: any) { + const defaultValueFunc = typeof def === "function" ? def : () => def; + + return new ZodDefaultOnMismatch({ + innerType: this, + defaultValue: defaultValueFunc, + typeName: ZodFirstPartyTypeKind.ZodDefaultOnMismatch, + }) as any; + } describe(description: string): this { const This = (this as any).constructor; @@ -3739,13 +3753,61 @@ export class ZodDefault extends ZodType< }; } -////////////////////////////////////// -////////////////////////////////////// -////////// ////////// -////////// ZodNaN ////////// -////////// ////////// -////////////////////////////////////// -////////////////////////////////////// +//////////////////////////////////////////// +//////////////////////////////////////////// +////////// ////////// +////////// ZodDefaultOnMismatch ////////// +////////// ////////// +//////////////////////////////////////////// +//////////////////////////////////////////// +export interface ZodDefaultOnMismatchDef + extends ZodTypeDef { + innerType: T; + defaultValue: () => util.noUndefined; + typeName: ZodFirstPartyTypeKind.ZodDefaultOnMismatch; +} + +export class ZodDefaultOnMismatch extends ZodType< + util.noUndefined, + ZodDefaultOnMismatchDef, + T["_input"] | undefined +> { + _parse(input: ParseInput): ParseReturnType { + const { ctx } = this._processInputParams(input); + const defaultValue = this._def.defaultValue(); + return this._def.innerType._parse({ + data: + ctx.parsedType !== getParsedType(defaultValue) + ? defaultValue + : ctx.data, + path: ctx.path, + parent: ctx, + }); + } + + removeDefault() { + return this._def.innerType; + } + + static create = ( + type: T, + params?: RawCreateParams + ): ZodOptional => { + return new ZodOptional({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodOptional, + ...processCreateParams(params), + }) as any; + }; +} + +///////////////////////////////////////// +///////////////////////////////////////// +////////// ////////// +////////// ZodNaN ////////// +////////// ////////// +///////////////////////////////////////// +///////////////////////////////////////// export interface ZodNaNDef extends ZodTypeDef { typeName: ZodFirstPartyTypeKind.ZodNaN; @@ -3865,6 +3927,7 @@ export enum ZodFirstPartyTypeKind { ZodOptional = "ZodOptional", ZodNullable = "ZodNullable", ZodDefault = "ZodDefault", + ZodDefaultOnMismatch = "ZodDefaultOnMismatch", ZodPromise = "ZodPromise", ZodBranded = "ZodBranded", } @@ -3899,6 +3962,7 @@ export type ZodFirstPartySchemaTypes = | ZodOptional | ZodNullable | ZodDefault + | ZodDefaultOnMismatch | ZodPromise | ZodBranded; From f4eda48e040ebeff17d91c30c291a22273098140 Mon Sep 17 00:00:00 2001 From: Sean Crowe Date: Wed, 2 Nov 2022 17:12:37 -0500 Subject: [PATCH 2/7] Update tests to new assertEqual method --- deno/lib/README.md | 6 ++-- deno/lib/__tests__/defaultOnMismatch.test.ts | 29 +++++++------------- src/__tests__/defaultOnMismatch.test.ts | 29 +++++++------------- 3 files changed, 23 insertions(+), 41 deletions(-) diff --git a/deno/lib/README.md b/deno/lib/README.md index 6da6c688c..ddea4a19b 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -779,7 +779,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; // string | null ``` @@ -1967,7 +1967,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; const petCat = (cat: Cat) => {}; @@ -1983,7 +1983,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; // {name: string} & {[symbol]: "Cat"} ``` diff --git a/deno/lib/__tests__/defaultOnMismatch.test.ts b/deno/lib/__tests__/defaultOnMismatch.test.ts index 3c08d5195..f46e08803 100644 --- a/deno/lib/__tests__/defaultOnMismatch.test.ts +++ b/deno/lib/__tests__/defaultOnMismatch.test.ts @@ -33,11 +33,9 @@ test("defaultOnMismatch with transform", () => { ); type inp = z.input; - const f1: util.AssertEqual = true; + util.assertEqual(true); type out = z.output; - const f2: util.AssertEqual = true; - f1; - f2; + util.assertEqual(true); }); test("defaultOnMismatch on existing optional", () => { @@ -51,22 +49,18 @@ test("defaultOnMismatch on existing optional", () => { ); type inp = z.input; - const f1: util.AssertEqual = true; + util.assertEqual(true); type out = z.output; - const f2: util.AssertEqual = true; - f1; - f2; + util.assertEqual(true); }); test("optional on defaultOnMismatch", () => { const stringWithDefault = z.string().defaultOnMismatch("asdf").optional(); type inp = z.input; - const f1: util.AssertEqual = true; + util.assertEqual(true); type out = z.output; - const f2: util.AssertEqual = true; - f1; - f2; + util.assertEqual(true); }); test("complex chain example", () => { @@ -89,8 +83,7 @@ test("removeDefault", () => { const stringWithRemovedDefault = z.string().defaultOnMismatch("asdf").removeDefault(); type out = z.output; - const f2: util.AssertEqual = true; - f2; + util.assertEqual(true); }); test("nested", () => { @@ -99,14 +92,12 @@ test("nested", () => { inner: "asdf", }); type input = z.input; - const f1: util.AssertEqual< + util.assertEqual< input, { inner?: string | undefined } | undefined - > = true; + >(true); type out = z.output; - const f2: util.AssertEqual = true; - f1; - f2; + util.assertEqual(true); expect(outer.parse(undefined)).toEqual({ inner: "asdf" }); expect(outer.parse({})).toEqual({ inner: "asdf" }); expect(outer.parse({ inner: undefined })).toEqual({ inner: "asdf" }); diff --git a/src/__tests__/defaultOnMismatch.test.ts b/src/__tests__/defaultOnMismatch.test.ts index f7225ec07..6016b435e 100644 --- a/src/__tests__/defaultOnMismatch.test.ts +++ b/src/__tests__/defaultOnMismatch.test.ts @@ -32,11 +32,9 @@ test("defaultOnMismatch with transform", () => { ); type inp = z.input; - const f1: util.AssertEqual = true; + util.assertEqual(true); type out = z.output; - const f2: util.AssertEqual = true; - f1; - f2; + util.assertEqual(true); }); test("defaultOnMismatch on existing optional", () => { @@ -50,22 +48,18 @@ test("defaultOnMismatch on existing optional", () => { ); type inp = z.input; - const f1: util.AssertEqual = true; + util.assertEqual(true); type out = z.output; - const f2: util.AssertEqual = true; - f1; - f2; + util.assertEqual(true); }); test("optional on defaultOnMismatch", () => { const stringWithDefault = z.string().defaultOnMismatch("asdf").optional(); type inp = z.input; - const f1: util.AssertEqual = true; + util.assertEqual(true); type out = z.output; - const f2: util.AssertEqual = true; - f1; - f2; + util.assertEqual(true); }); test("complex chain example", () => { @@ -88,8 +82,7 @@ test("removeDefault", () => { const stringWithRemovedDefault = z.string().defaultOnMismatch("asdf").removeDefault(); type out = z.output; - const f2: util.AssertEqual = true; - f2; + util.assertEqual(true); }); test("nested", () => { @@ -98,14 +91,12 @@ test("nested", () => { inner: "asdf", }); type input = z.input; - const f1: util.AssertEqual< + util.assertEqual< input, { inner?: string | undefined } | undefined - > = true; + >(true); type out = z.output; - const f2: util.AssertEqual = true; - f1; - f2; + util.assertEqual(true); expect(outer.parse(undefined)).toEqual({ inner: "asdf" }); expect(outer.parse({})).toEqual({ inner: "asdf" }); expect(outer.parse({ inner: undefined })).toEqual({ inner: "asdf" }); From 2a6ba8cfcc94193ecb1dfb07d51f4ee0061ce131 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Mon, 14 Nov 2022 00:28:12 -0800 Subject: [PATCH 3/7] Add catch --- README.md | 33 +++++- deno/lib/README.md | 33 +++++- deno/lib/__tests__/catch.test.ts | 139 ++++++++++++++++++++++++ deno/lib/__tests__/firstparty.test.ts | 2 +- deno/lib/types.ts | 97 ++++++++++------- playground.ts | 38 +++++++ src/__tests__/catch.test.ts | 138 +++++++++++++++++++++++ src/__tests__/defaultOnMismatch.test.ts | 139 ------------------------ src/__tests__/firstparty.test.ts | 2 +- src/types.ts | 97 ++++++++++------- 10 files changed, 501 insertions(+), 217 deletions(-) create mode 100644 deno/lib/__tests__/catch.test.ts create mode 100644 playground.ts create mode 100644 src/__tests__/catch.test.ts delete mode 100644 src/__tests__/defaultOnMismatch.test.ts diff --git a/README.md b/README.md index ddea4a19b..a2c3e42a0 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ - [.superRefine](#superRefine) - [.transform](#transform) - [.default](#default) + - [.catch](#catch) - [.optional](#optional) - [.nullable](#nullable) - [.nullish](#nullish) @@ -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. @@ -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. diff --git a/deno/lib/README.md b/deno/lib/README.md index ddea4a19b..a2c3e42a0 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -100,6 +100,7 @@ - [.superRefine](#superRefine) - [.transform](#transform) - [.default](#default) + - [.catch](#catch) - [.optional](#optional) - [.nullable](#nullable) - [.nullish](#nullish) @@ -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. @@ -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. diff --git a/deno/lib/__tests__/catch.test.ts b/deno/lib/__tests__/catch.test.ts new file mode 100644 index 000000000..3aae31a85 --- /dev/null +++ b/deno/lib/__tests__/catch.test.ts @@ -0,0 +1,139 @@ +// @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("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; + util.assertEqual(true); + type out = z.output; + util.assertEqual(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; + util.assertEqual(true); + type out = z.output; + util.assertEqual(true); +}); + +test("optional on catch", () => { + const stringWithDefault = z.string().catch("asdf").optional(); + + type inp = z.input; + util.assertEqual(true); + type out = z.output; + util.assertEqual(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; + util.assertEqual(true); +}); + +test("nested", () => { + const inner = z.string().catch("asdf"); + const outer = z.object({ inner }).catch({ + inner: "asdf", + }); + type input = z.input; + util.assertEqual(true); + type out = z.output; + util.assertEqual(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(), { + defaultValue: "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" }); +}); diff --git a/deno/lib/__tests__/firstparty.test.ts b/deno/lib/__tests__/firstparty.test.ts index e44047adb..a6bbb789c 100644 --- a/deno/lib/__tests__/firstparty.test.ts +++ b/deno/lib/__tests__/firstparty.test.ts @@ -69,7 +69,7 @@ test("first party switch", () => { break; case z.ZodFirstPartyTypeKind.ZodDefault: break; - case z.ZodFirstPartyTypeKind.ZodDefaultOnMismatch: + case z.ZodFirstPartyTypeKind.ZodCatch: break; case z.ZodFirstPartyTypeKind.ZodPromise: break; diff --git a/deno/lib/types.ts b/deno/lib/types.ts index f049aa184..4a6f1d752 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -369,7 +369,7 @@ export abstract class ZodType< this.transform = this.transform.bind(this); this.brand = this.brand.bind(this); this.default = this.default.bind(this); - this.defaultOnMismatch = this.defaultOnMismatch.bind(this); + this.catch = this.catch.bind(this); this.describe = this.describe.bind(this); this.isNullable = this.isNullable.bind(this); this.isOptional = this.isOptional.bind(this); @@ -428,17 +428,15 @@ export abstract class ZodType< ...processCreateParams(undefined), }); } - defaultOnMismatch(def: util.noUndefined): ZodDefaultOnMismatch; - defaultOnMismatch( - def: () => util.noUndefined - ): ZodDefaultOnMismatch; - defaultOnMismatch(def: any) { + catch(def: Input): ZodCatch; + catch(def: () => Input): ZodCatch; + catch(def: any) { const defaultValueFunc = typeof def === "function" ? def : () => def; - return new ZodDefaultOnMismatch({ + return new ZodCatch({ innerType: this, defaultValue: defaultValueFunc, - typeName: ZodFirstPartyTypeKind.ZodDefaultOnMismatch, + typeName: ZodFirstPartyTypeKind.ZodCatch, }) as any; } @@ -3743,46 +3741,65 @@ export class ZodDefault extends ZodType< static create = ( type: T, - params?: RawCreateParams - ): ZodOptional => { - return new ZodOptional({ + params: RawCreateParams & { + default: T["_input"] | (() => util.noUndefined); + } + ): ZodDefault => { + return new ZodDefault({ innerType: type, - typeName: ZodFirstPartyTypeKind.ZodOptional, + typeName: ZodFirstPartyTypeKind.ZodDefault, + defaultValue: + typeof params.default === "function" + ? params.default + : () => params.default as any, ...processCreateParams(params), }) as any; }; } -//////////////////////////////////////////// -//////////////////////////////////////////// -////////// ////////// -////////// ZodDefaultOnMismatch ////////// -////////// ////////// -//////////////////////////////////////////// -//////////////////////////////////////////// -export interface ZodDefaultOnMismatchDef +////////////////////////////////////////// +////////////////////////////////////////// +////////// ////////// +////////// ZodCatch ////////// +////////// ////////// +////////////////////////////////////////// +////////////////////////////////////////// +export interface ZodCatchDef extends ZodTypeDef { innerType: T; - defaultValue: () => util.noUndefined; - typeName: ZodFirstPartyTypeKind.ZodDefaultOnMismatch; + defaultValue: () => T["_input"]; + typeName: ZodFirstPartyTypeKind.ZodCatch; } -export class ZodDefaultOnMismatch extends ZodType< +export class ZodCatch extends ZodType< util.noUndefined, - ZodDefaultOnMismatchDef, + ZodCatchDef, T["_input"] | undefined > { _parse(input: ParseInput): ParseReturnType { const { ctx } = this._processInputParams(input); - const defaultValue = this._def.defaultValue(); - return this._def.innerType._parse({ - data: - ctx.parsedType !== getParsedType(defaultValue) - ? defaultValue - : ctx.data, + + const result = this._def.innerType._parse({ + data: ctx.data, path: ctx.path, parent: ctx, }); + + if (isAsync(result)) { + return result.then((result) => { + const defaultValue = this._def.defaultValue(); + return { + status: "valid", + value: result.status === "valid" ? result.value : defaultValue, + }; + }); + } else { + const defaultValue = this._def.defaultValue(); + return { + status: "valid", + value: result.status === "valid" ? result.value : defaultValue, + }; + } } removeDefault() { @@ -3791,13 +3808,19 @@ export class ZodDefaultOnMismatch extends ZodType< static create = ( type: T, - params?: RawCreateParams - ): ZodOptional => { - return new ZodOptional({ + params: RawCreateParams & { + defaultValue: T["_input"] | (() => T["_input"]); + } + ): ZodCatch => { + return new ZodCatch({ innerType: type, - typeName: ZodFirstPartyTypeKind.ZodOptional, + typeName: ZodFirstPartyTypeKind.ZodCatch, + defaultValue: + typeof params.defaultValue === "function" + ? params.defaultValue + : () => params.defaultValue, ...processCreateParams(params), - }) as any; + }); }; } @@ -3927,7 +3950,7 @@ export enum ZodFirstPartyTypeKind { ZodOptional = "ZodOptional", ZodNullable = "ZodNullable", ZodDefault = "ZodDefault", - ZodDefaultOnMismatch = "ZodDefaultOnMismatch", + ZodCatch = "ZodCatch", ZodPromise = "ZodPromise", ZodBranded = "ZodBranded", } @@ -3962,7 +3985,7 @@ export type ZodFirstPartySchemaTypes = | ZodOptional | ZodNullable | ZodDefault - | ZodDefaultOnMismatch + | ZodCatch | ZodPromise | ZodBranded; diff --git a/playground.ts b/playground.ts new file mode 100644 index 000000000..58a4ecd2d --- /dev/null +++ b/playground.ts @@ -0,0 +1,38 @@ +import { z, ZodFormattedError } from "./src"; + +// const type = z.intersection( +// z.object({ a: z.null() }), +// z.record( +// z.string().refine((s): s is "b" => s === "b"), +// z.undefined() +// ) +// ); + +// type type = z.infer; +// const input: type = { a: null, b: undefined }; +// const result = type.parse(input); +// console.log(result); + +// type myType = Partial>; +// const myData: myType = { b: "" }; +// Type '{ b: string; }' is not assignable to type 'Partial>'. +// Object literal may only specify known properties, and 'b' does not exist in type 'Partial>'.ts + +// const myEnum = z.enum(["a", "b"]); +// const myRecord = z.record(myEnum, z.string()); +// console.log(myRecord.parse({ c: "asfd" })); + +// const result = z +// .string() +// .transform((x) => x.length) +// .pipe(z.number()) +// .parse("asdf"); +// console.log(result); + +async function main() { + const schema = z.string().catch("1234"); + + const result = await schema.parse(1234); + console.log(result); +} +main(); diff --git a/src/__tests__/catch.test.ts b/src/__tests__/catch.test.ts new file mode 100644 index 000000000..a34faeb0b --- /dev/null +++ b/src/__tests__/catch.test.ts @@ -0,0 +1,138 @@ +// @ts-ignore TS6133 +import { expect, test } from "@jest/globals"; + +import { z } from ".."; +import { util } from "../helpers/util"; + +test("basic catch", () => { + expect(z.string().catch("default").parse(undefined)).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; + util.assertEqual(true); + type out = z.output; + util.assertEqual(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; + util.assertEqual(true); + type out = z.output; + util.assertEqual(true); +}); + +test("optional on catch", () => { + const stringWithDefault = z.string().catch("asdf").optional(); + + type inp = z.input; + util.assertEqual(true); + type out = z.output; + util.assertEqual(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; + util.assertEqual(true); +}); + +test("nested", () => { + const inner = z.string().catch("asdf"); + const outer = z.object({ inner }).catch({ + inner: "asdf", + }); + type input = z.input; + util.assertEqual(true); + type out = z.output; + util.assertEqual(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(), { + defaultValue: "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" }); +}); diff --git a/src/__tests__/defaultOnMismatch.test.ts b/src/__tests__/defaultOnMismatch.test.ts deleted file mode 100644 index 6016b435e..000000000 --- a/src/__tests__/defaultOnMismatch.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -// @ts-ignore TS6133 -import { expect, test } from "@jest/globals"; - -import { z } from ".."; -import { util } from "../helpers/util"; - -test("basic defaultOnMismatch", () => { - expect(z.string().defaultOnMismatch("default").parse(undefined)).toBe("default"); -}); - -test("defaultOnMismatch replace wrong types", () => { - expect(z.string().defaultOnMismatch("default").parse(true)).toBe("default"); - expect(z.string().defaultOnMismatch("default").parse(true)).toBe("default"); - expect(z.string().defaultOnMismatch("default").parse(15)).toBe("default"); - expect(z.string().defaultOnMismatch("default").parse([])).toBe("default"); - expect(z.string().defaultOnMismatch("default").parse(new Map())).toBe("default"); - expect(z.string().defaultOnMismatch("default").parse(new Set())).toBe("default"); - expect(z.string().defaultOnMismatch("default").parse({})).toBe("default"); -}); - -test("defaultOnMismatch with transform", () => { - const stringWithDefault = z - .string() - .transform((val) => val.toUpperCase()) - .defaultOnMismatch("default"); - expect(stringWithDefault.parse(undefined)).toBe("DEFAULT"); - expect(stringWithDefault.parse(15)).toBe("DEFAULT"); - expect(stringWithDefault).toBeInstanceOf(z.ZodDefaultOnMismatch); - expect(stringWithDefault._def.innerType).toBeInstanceOf(z.ZodEffects); - expect(stringWithDefault._def.innerType._def.schema).toBeInstanceOf( - z.ZodSchema - ); - - type inp = z.input; - util.assertEqual(true); - type out = z.output; - util.assertEqual(true); -}); - -test("defaultOnMismatch on existing optional", () => { - const stringWithDefault = z.string().optional().defaultOnMismatch("asdf"); - expect(stringWithDefault.parse(undefined)).toBe("asdf"); - expect(stringWithDefault.parse(15)).toBe("asdf"); - expect(stringWithDefault).toBeInstanceOf(z.ZodDefaultOnMismatch); - expect(stringWithDefault._def.innerType).toBeInstanceOf(z.ZodOptional); - expect(stringWithDefault._def.innerType._def.innerType).toBeInstanceOf( - z.ZodString - ); - - type inp = z.input; - util.assertEqual(true); - type out = z.output; - util.assertEqual(true); -}); - -test("optional on defaultOnMismatch", () => { - const stringWithDefault = z.string().defaultOnMismatch("asdf").optional(); - - type inp = z.input; - util.assertEqual(true); - type out = z.output; - util.assertEqual(true); -}); - -test("complex chain example", () => { - const complex = z - .string() - .defaultOnMismatch("asdf") - .transform((val) => val + "!") - .transform((val) => val.toUpperCase()) - .defaultOnMismatch("qwer") - .removeDefault() - .optional() - .defaultOnMismatch("asdfasdf"); - - expect(complex.parse(undefined)).toBe("ASDFASDF!"); - expect(complex.parse(15)).toBe("ASDFASDF!"); - expect(complex.parse(true)).toBe("ASDFASDF!"); -}); - -test("removeDefault", () => { - const stringWithRemovedDefault = z.string().defaultOnMismatch("asdf").removeDefault(); - - type out = z.output; - util.assertEqual(true); -}); - -test("nested", () => { - const inner = z.string().defaultOnMismatch("asdf"); - const outer = z.object({ inner }).defaultOnMismatch({ - inner: "asdf", - }); - type input = z.input; - util.assertEqual< - input, - { inner?: string | undefined } | undefined - >(true); - type out = z.output; - util.assertEqual(true); - expect(outer.parse(undefined)).toEqual({ inner: "asdf" }); - expect(outer.parse({})).toEqual({ inner: "asdf" }); - expect(outer.parse({ inner: undefined })).toEqual({ inner: "asdf" }); -}); - -test("chained defaultOnMismatch", () => { - const stringWithDefault = z.string().defaultOnMismatch("inner").defaultOnMismatch("outer"); - const result = stringWithDefault.parse(undefined); - expect(result).toEqual("outer"); - const resultDiff = stringWithDefault.parse(5); - expect(resultDiff).toEqual("outer"); -}); - -test("factory", () => { - z.ZodDefaultOnMismatch.create(z.string()).parse(undefined); -}); - -test("native enum", () => { - enum Fruits { - apple = "apple", - orange = "orange", - } - - const schema = z.object({ - fruit: z.nativeEnum(Fruits).defaultOnMismatch(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"]).defaultOnMismatch("apple"), - }); - - expect(schema.parse({})).toEqual({ fruit: "apple" }); - expect(schema.parse({fruit:true})).toEqual({ fruit: "apple" }); - expect(schema.parse({fruit:15})).toEqual({ fruit: "apple" }); -}); diff --git a/src/__tests__/firstparty.test.ts b/src/__tests__/firstparty.test.ts index 3fbac741a..dd359a1c9 100644 --- a/src/__tests__/firstparty.test.ts +++ b/src/__tests__/firstparty.test.ts @@ -68,7 +68,7 @@ test("first party switch", () => { break; case z.ZodFirstPartyTypeKind.ZodDefault: break; - case z.ZodFirstPartyTypeKind.ZodDefaultOnMismatch: + case z.ZodFirstPartyTypeKind.ZodCatch: break; case z.ZodFirstPartyTypeKind.ZodPromise: break; diff --git a/src/types.ts b/src/types.ts index bf024f58f..99874f014 100644 --- a/src/types.ts +++ b/src/types.ts @@ -369,7 +369,7 @@ export abstract class ZodType< this.transform = this.transform.bind(this); this.brand = this.brand.bind(this); this.default = this.default.bind(this); - this.defaultOnMismatch = this.defaultOnMismatch.bind(this); + this.catch = this.catch.bind(this); this.describe = this.describe.bind(this); this.isNullable = this.isNullable.bind(this); this.isOptional = this.isOptional.bind(this); @@ -428,17 +428,15 @@ export abstract class ZodType< ...processCreateParams(undefined), }); } - defaultOnMismatch(def: util.noUndefined): ZodDefaultOnMismatch; - defaultOnMismatch( - def: () => util.noUndefined - ): ZodDefaultOnMismatch; - defaultOnMismatch(def: any) { + catch(def: Input): ZodCatch; + catch(def: () => Input): ZodCatch; + catch(def: any) { const defaultValueFunc = typeof def === "function" ? def : () => def; - return new ZodDefaultOnMismatch({ + return new ZodCatch({ innerType: this, defaultValue: defaultValueFunc, - typeName: ZodFirstPartyTypeKind.ZodDefaultOnMismatch, + typeName: ZodFirstPartyTypeKind.ZodCatch, }) as any; } @@ -3743,46 +3741,65 @@ export class ZodDefault extends ZodType< static create = ( type: T, - params?: RawCreateParams - ): ZodOptional => { - return new ZodOptional({ + params: RawCreateParams & { + default: T["_input"] | (() => util.noUndefined); + } + ): ZodDefault => { + return new ZodDefault({ innerType: type, - typeName: ZodFirstPartyTypeKind.ZodOptional, + typeName: ZodFirstPartyTypeKind.ZodDefault, + defaultValue: + typeof params.default === "function" + ? params.default + : () => params.default as any, ...processCreateParams(params), }) as any; }; } -//////////////////////////////////////////// -//////////////////////////////////////////// -////////// ////////// -////////// ZodDefaultOnMismatch ////////// -////////// ////////// -//////////////////////////////////////////// -//////////////////////////////////////////// -export interface ZodDefaultOnMismatchDef +////////////////////////////////////////// +////////////////////////////////////////// +////////// ////////// +////////// ZodCatch ////////// +////////// ////////// +////////////////////////////////////////// +////////////////////////////////////////// +export interface ZodCatchDef extends ZodTypeDef { innerType: T; - defaultValue: () => util.noUndefined; - typeName: ZodFirstPartyTypeKind.ZodDefaultOnMismatch; + defaultValue: () => T["_input"]; + typeName: ZodFirstPartyTypeKind.ZodCatch; } -export class ZodDefaultOnMismatch extends ZodType< +export class ZodCatch extends ZodType< util.noUndefined, - ZodDefaultOnMismatchDef, + ZodCatchDef, T["_input"] | undefined > { _parse(input: ParseInput): ParseReturnType { const { ctx } = this._processInputParams(input); - const defaultValue = this._def.defaultValue(); - return this._def.innerType._parse({ - data: - ctx.parsedType !== getParsedType(defaultValue) - ? defaultValue - : ctx.data, + + const result = this._def.innerType._parse({ + data: ctx.data, path: ctx.path, parent: ctx, }); + + if (isAsync(result)) { + return result.then((result) => { + const defaultValue = this._def.defaultValue(); + return { + status: "valid", + value: result.status === "valid" ? result.value : defaultValue, + }; + }); + } else { + const defaultValue = this._def.defaultValue(); + return { + status: "valid", + value: result.status === "valid" ? result.value : defaultValue, + }; + } } removeDefault() { @@ -3791,13 +3808,19 @@ export class ZodDefaultOnMismatch extends ZodType< static create = ( type: T, - params?: RawCreateParams - ): ZodOptional => { - return new ZodOptional({ + params: RawCreateParams & { + defaultValue: T["_input"] | (() => T["_input"]); + } + ): ZodCatch => { + return new ZodCatch({ innerType: type, - typeName: ZodFirstPartyTypeKind.ZodOptional, + typeName: ZodFirstPartyTypeKind.ZodCatch, + defaultValue: + typeof params.defaultValue === "function" + ? params.defaultValue + : () => params.defaultValue, ...processCreateParams(params), - }) as any; + }); }; } @@ -3927,7 +3950,7 @@ export enum ZodFirstPartyTypeKind { ZodOptional = "ZodOptional", ZodNullable = "ZodNullable", ZodDefault = "ZodDefault", - ZodDefaultOnMismatch = "ZodDefaultOnMismatch", + ZodCatch = "ZodCatch", ZodPromise = "ZodPromise", ZodBranded = "ZodBranded", } @@ -3962,7 +3985,7 @@ export type ZodFirstPartySchemaTypes = | ZodOptional | ZodNullable | ZodDefault - | ZodDefaultOnMismatch + | ZodCatch | ZodPromise | ZodBranded; From 60455ad90675c0f2537825f330fc3b7c46344ffc Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Mon, 14 Nov 2022 00:29:45 -0800 Subject: [PATCH 4/7] Add async test --- deno/lib/__tests__/catch.test.ts | 5 +++++ src/__tests__/catch.test.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/deno/lib/__tests__/catch.test.ts b/deno/lib/__tests__/catch.test.ts index 3aae31a85..e13fc3151 100644 --- a/deno/lib/__tests__/catch.test.ts +++ b/deno/lib/__tests__/catch.test.ts @@ -9,6 +9,11 @@ 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"); diff --git a/src/__tests__/catch.test.ts b/src/__tests__/catch.test.ts index a34faeb0b..02dcae16d 100644 --- a/src/__tests__/catch.test.ts +++ b/src/__tests__/catch.test.ts @@ -8,6 +8,11 @@ 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"); From 4b185b557838ac2cc3f517bbdd0d74508be998bc Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Mon, 14 Nov 2022 00:31:57 -0800 Subject: [PATCH 5/7] Fix default tests --- deno/lib/__tests__/default.test.ts | 4 +++- deno/lib/types.ts | 8 ++++---- src/__tests__/default.test.ts | 4 +++- src/types.ts | 8 ++++---- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/deno/lib/__tests__/default.test.ts b/deno/lib/__tests__/default.test.ts index 04579a66e..6ad12cae7 100644 --- a/deno/lib/__tests__/default.test.ts +++ b/deno/lib/__tests__/default.test.ts @@ -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", () => { diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 4a6f1d752..3b3587eaf 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -3809,16 +3809,16 @@ export class ZodCatch extends ZodType< static create = ( type: T, params: RawCreateParams & { - defaultValue: T["_input"] | (() => T["_input"]); + default: T["_input"] | (() => T["_input"]); } ): ZodCatch => { return new ZodCatch({ innerType: type, typeName: ZodFirstPartyTypeKind.ZodCatch, defaultValue: - typeof params.defaultValue === "function" - ? params.defaultValue - : () => params.defaultValue, + typeof params.default === "function" + ? params.default + : () => params.default, ...processCreateParams(params), }); }; diff --git a/src/__tests__/default.test.ts b/src/__tests__/default.test.ts index b3fa33eba..bd03cbd85 100644 --- a/src/__tests__/default.test.ts +++ b/src/__tests__/default.test.ts @@ -91,7 +91,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", () => { diff --git a/src/types.ts b/src/types.ts index 99874f014..fc9c4f2c0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3809,16 +3809,16 @@ export class ZodCatch extends ZodType< static create = ( type: T, params: RawCreateParams & { - defaultValue: T["_input"] | (() => T["_input"]); + default: T["_input"] | (() => T["_input"]); } ): ZodCatch => { return new ZodCatch({ innerType: type, typeName: ZodFirstPartyTypeKind.ZodCatch, defaultValue: - typeof params.defaultValue === "function" - ? params.defaultValue - : () => params.defaultValue, + typeof params.default === "function" + ? params.default + : () => params.default, ...processCreateParams(params), }); }; From 1b12d8e8ca775b01e533303b9186c66cbbabaaa1 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Mon, 14 Nov 2022 00:35:29 -0800 Subject: [PATCH 6/7] Remove unnecessary test file --- deno/lib/__tests__/defaultOnMismatch.test.ts | 140 ------------------- 1 file changed, 140 deletions(-) delete mode 100644 deno/lib/__tests__/defaultOnMismatch.test.ts diff --git a/deno/lib/__tests__/defaultOnMismatch.test.ts b/deno/lib/__tests__/defaultOnMismatch.test.ts deleted file mode 100644 index f46e08803..000000000 --- a/deno/lib/__tests__/defaultOnMismatch.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -// @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 defaultOnMismatch", () => { - expect(z.string().defaultOnMismatch("default").parse(undefined)).toBe("default"); -}); - -test("defaultOnMismatch replace wrong types", () => { - expect(z.string().defaultOnMismatch("default").parse(true)).toBe("default"); - expect(z.string().defaultOnMismatch("default").parse(true)).toBe("default"); - expect(z.string().defaultOnMismatch("default").parse(15)).toBe("default"); - expect(z.string().defaultOnMismatch("default").parse([])).toBe("default"); - expect(z.string().defaultOnMismatch("default").parse(new Map())).toBe("default"); - expect(z.string().defaultOnMismatch("default").parse(new Set())).toBe("default"); - expect(z.string().defaultOnMismatch("default").parse({})).toBe("default"); -}); - -test("defaultOnMismatch with transform", () => { - const stringWithDefault = z - .string() - .transform((val) => val.toUpperCase()) - .defaultOnMismatch("default"); - expect(stringWithDefault.parse(undefined)).toBe("DEFAULT"); - expect(stringWithDefault.parse(15)).toBe("DEFAULT"); - expect(stringWithDefault).toBeInstanceOf(z.ZodDefaultOnMismatch); - expect(stringWithDefault._def.innerType).toBeInstanceOf(z.ZodEffects); - expect(stringWithDefault._def.innerType._def.schema).toBeInstanceOf( - z.ZodSchema - ); - - type inp = z.input; - util.assertEqual(true); - type out = z.output; - util.assertEqual(true); -}); - -test("defaultOnMismatch on existing optional", () => { - const stringWithDefault = z.string().optional().defaultOnMismatch("asdf"); - expect(stringWithDefault.parse(undefined)).toBe("asdf"); - expect(stringWithDefault.parse(15)).toBe("asdf"); - expect(stringWithDefault).toBeInstanceOf(z.ZodDefaultOnMismatch); - expect(stringWithDefault._def.innerType).toBeInstanceOf(z.ZodOptional); - expect(stringWithDefault._def.innerType._def.innerType).toBeInstanceOf( - z.ZodString - ); - - type inp = z.input; - util.assertEqual(true); - type out = z.output; - util.assertEqual(true); -}); - -test("optional on defaultOnMismatch", () => { - const stringWithDefault = z.string().defaultOnMismatch("asdf").optional(); - - type inp = z.input; - util.assertEqual(true); - type out = z.output; - util.assertEqual(true); -}); - -test("complex chain example", () => { - const complex = z - .string() - .defaultOnMismatch("asdf") - .transform((val) => val + "!") - .transform((val) => val.toUpperCase()) - .defaultOnMismatch("qwer") - .removeDefault() - .optional() - .defaultOnMismatch("asdfasdf"); - - expect(complex.parse(undefined)).toBe("ASDFASDF!"); - expect(complex.parse(15)).toBe("ASDFASDF!"); - expect(complex.parse(true)).toBe("ASDFASDF!"); -}); - -test("removeDefault", () => { - const stringWithRemovedDefault = z.string().defaultOnMismatch("asdf").removeDefault(); - - type out = z.output; - util.assertEqual(true); -}); - -test("nested", () => { - const inner = z.string().defaultOnMismatch("asdf"); - const outer = z.object({ inner }).defaultOnMismatch({ - inner: "asdf", - }); - type input = z.input; - util.assertEqual< - input, - { inner?: string | undefined } | undefined - >(true); - type out = z.output; - util.assertEqual(true); - expect(outer.parse(undefined)).toEqual({ inner: "asdf" }); - expect(outer.parse({})).toEqual({ inner: "asdf" }); - expect(outer.parse({ inner: undefined })).toEqual({ inner: "asdf" }); -}); - -test("chained defaultOnMismatch", () => { - const stringWithDefault = z.string().defaultOnMismatch("inner").defaultOnMismatch("outer"); - const result = stringWithDefault.parse(undefined); - expect(result).toEqual("outer"); - const resultDiff = stringWithDefault.parse(5); - expect(resultDiff).toEqual("outer"); -}); - -test("factory", () => { - z.ZodDefaultOnMismatch.create(z.string()).parse(undefined); -}); - -test("native enum", () => { - enum Fruits { - apple = "apple", - orange = "orange", - } - - const schema = z.object({ - fruit: z.nativeEnum(Fruits).defaultOnMismatch(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"]).defaultOnMismatch("apple"), - }); - - expect(schema.parse({})).toEqual({ fruit: "apple" }); - expect(schema.parse({fruit:true})).toEqual({ fruit: "apple" }); - expect(schema.parse({fruit:15})).toEqual({ fruit: "apple" }); -}); From f6e14d26ec81e6df5d83c95ec2ca7af662315b2c Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Mon, 14 Nov 2022 00:39:09 -0800 Subject: [PATCH 7/7] Update test --- deno/lib/__tests__/catch.test.ts | 2 +- src/__tests__/catch.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deno/lib/__tests__/catch.test.ts b/deno/lib/__tests__/catch.test.ts index e13fc3151..b2910a0dc 100644 --- a/deno/lib/__tests__/catch.test.ts +++ b/deno/lib/__tests__/catch.test.ts @@ -115,7 +115,7 @@ test("chained catch", () => { test("factory", () => { z.ZodCatch.create(z.string(), { - defaultValue: "asdf", + default: "asdf", }).parse(undefined); }); diff --git a/src/__tests__/catch.test.ts b/src/__tests__/catch.test.ts index 02dcae16d..22c2e348d 100644 --- a/src/__tests__/catch.test.ts +++ b/src/__tests__/catch.test.ts @@ -114,7 +114,7 @@ test("chained catch", () => { test("factory", () => { z.ZodCatch.create(z.string(), { - defaultValue: "asdf", + default: "asdf", }).parse(undefined); });