diff --git a/deno/lib/__tests__/transformer.test.ts b/deno/lib/__tests__/transformer.test.ts index 818aa49cb..0fe586aa7 100644 --- a/deno/lib/__tests__/transformer.test.ts +++ b/deno/lib/__tests__/transformer.test.ts @@ -212,6 +212,73 @@ test("async preprocess", async () => { expect(value).toEqual(["asdf"]); }); +test("preprocess ctx.addIssue with parse", () => { + expect(() => { + z.preprocess((data, ctx) => { + ctx?.addIssue({ + code: "custom", + message: `${data} is not one of our allowed strings`, + }); + return data; + }, z.string()).parse("asdf"); + }).toThrow( + JSON.stringify( + [ + { + code: "custom", + message: "asdf is not one of our allowed strings", + path: [], + }, + ], + null, + 2 + ) + ); +}); + +test("preprocess ctx.addIssue with parseAsync", async () => { + const result = await z + .preprocess((data, ctx) => { + ctx?.addIssue({ + code: "custom", + message: `${data} is not one of our allowed strings`, + }); + return data; + }, z.string()) + .safeParseAsync("asdf"); + + expect(JSON.parse(JSON.stringify(result))).toEqual({ + success: false, + error: { + issues: [ + { + code: "custom", + message: "asdf is not one of our allowed strings", + path: [], + }, + ], + name: "ZodError", + }, + }); +}); + +test("z.NEVER in preprocess", async () => { + const foo = z.preprocess((val, ctx) => { + if (!val) { + ctx?.addIssue({ code: z.ZodIssueCode.custom, message: "bad" }); + return z.NEVER; + } + return val; + }, z.number()); + + type foo = z.infer; + util.assertEqual(true); + const arg = foo.safeParse(undefined); + if (!arg.success) { + expect(arg.error.issues[0].message).toEqual("bad"); + } +}); + test("short circuit on dirty", () => { const schema = z .string() diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 1dfea62c2..3e9e686f8 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -4186,7 +4186,7 @@ export type TransformEffect = { }; export type PreprocessEffect = { type: "preprocess"; - transform: (arg: T) => any; + transform: (arg: T, ctx: RefinementCtx) => any; }; export type Effect = | RefinementEffect @@ -4220,8 +4220,30 @@ export class ZodEffects< const effect = this._def.effect || null; + const checkCtx: RefinementCtx = { + addIssue: (arg: IssueData) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } else { + status.dirty(); + } + }, + get path() { + return ctx.path; + }, + }; + + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "preprocess") { - const processed = effect.transform(ctx.data); + const processed = effect.transform(ctx.data, checkCtx); + if (ctx.common.issues.length) { + return { + status: "dirty", + value: ctx.data, + }; + } if (ctx.common.async) { return Promise.resolve(processed).then((processed) => { @@ -4239,22 +4261,6 @@ export class ZodEffects< }); } } - - const checkCtx: RefinementCtx = { - addIssue: (arg: IssueData) => { - addIssueToContext(ctx, arg); - if (arg.fatal) { - status.abort(); - } else { - status.dirty(); - } - }, - get path() { - return ctx.path; - }, - }; - - checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); if (effect.type === "refinement") { const executeRefinement = ( acc: unknown @@ -4346,7 +4352,7 @@ export class ZodEffects< }; static createWithPreprocess = ( - preprocess: (arg: unknown) => unknown, + preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams ): ZodEffects => { diff --git a/playground.ts b/playground.ts index a135d7707..d17b18eb4 100644 --- a/playground.ts +++ b/playground.ts @@ -1,49 +1,2 @@ import { z } from "./src"; z; - -console.log( - z - .string() - .toUpperCase() - .pipe(z.enum(["DE", "EN"])) - .parse("de") -); - -function recursive( - callback: (schema: G) => T -): T { - return "asdf" as any; -} - -z.string(); - -const cat = recursive((type) => { - return z.object({ - name: z.string(), - subcategories: type, - }); -}); -type cat = z.infer; //["subcategories"]; -declare let fido: cat; -fido; -fido.subcategories![0]; - -declare const __nominal__type: unique symbol; -declare const __nominal__type2: unique symbol; - -const arg = { - a: "asdf", - b: "asdf", - c: "asdf", - ["$type"]: () => {}, - ["@@type"]: () => {}, - ["{type}"]: 1324, -}; - -arg; - -const kwarg = { - [__nominal__type2]: "asdf", -}; - -type aklmdf = typeof arg extends typeof kwarg ? true : false; diff --git a/src/__tests__/transformer.test.ts b/src/__tests__/transformer.test.ts index 8e1f2a598..5cf606c84 100644 --- a/src/__tests__/transformer.test.ts +++ b/src/__tests__/transformer.test.ts @@ -211,6 +211,73 @@ test("async preprocess", async () => { expect(value).toEqual(["asdf"]); }); +test("preprocess ctx.addIssue with parse", () => { + expect(() => { + z.preprocess((data, ctx) => { + ctx?.addIssue({ + code: "custom", + message: `${data} is not one of our allowed strings`, + }); + return data; + }, z.string()).parse("asdf"); + }).toThrow( + JSON.stringify( + [ + { + code: "custom", + message: "asdf is not one of our allowed strings", + path: [], + }, + ], + null, + 2 + ) + ); +}); + +test("preprocess ctx.addIssue with parseAsync", async () => { + const result = await z + .preprocess((data, ctx) => { + ctx?.addIssue({ + code: "custom", + message: `${data} is not one of our allowed strings`, + }); + return data; + }, z.string()) + .safeParseAsync("asdf"); + + expect(JSON.parse(JSON.stringify(result))).toEqual({ + success: false, + error: { + issues: [ + { + code: "custom", + message: "asdf is not one of our allowed strings", + path: [], + }, + ], + name: "ZodError", + }, + }); +}); + +test("z.NEVER in preprocess", async () => { + const foo = z.preprocess((val, ctx) => { + if (!val) { + ctx?.addIssue({ code: z.ZodIssueCode.custom, message: "bad" }); + return z.NEVER; + } + return val; + }, z.number()); + + type foo = z.infer; + util.assertEqual(true); + const arg = foo.safeParse(undefined); + if (!arg.success) { + expect(arg.error.issues[0].message).toEqual("bad"); + } +}); + test("short circuit on dirty", () => { const schema = z .string() diff --git a/src/types.ts b/src/types.ts index f084b0ded..537e361f0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4186,7 +4186,7 @@ export type TransformEffect = { }; export type PreprocessEffect = { type: "preprocess"; - transform: (arg: T) => any; + transform: (arg: T, ctx: RefinementCtx) => any; }; export type Effect = | RefinementEffect @@ -4220,8 +4220,30 @@ export class ZodEffects< const effect = this._def.effect || null; + const checkCtx: RefinementCtx = { + addIssue: (arg: IssueData) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } else { + status.dirty(); + } + }, + get path() { + return ctx.path; + }, + }; + + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "preprocess") { - const processed = effect.transform(ctx.data); + const processed = effect.transform(ctx.data, checkCtx); + if (ctx.common.issues.length) { + return { + status: "dirty", + value: ctx.data, + }; + } if (ctx.common.async) { return Promise.resolve(processed).then((processed) => { @@ -4239,22 +4261,6 @@ export class ZodEffects< }); } } - - const checkCtx: RefinementCtx = { - addIssue: (arg: IssueData) => { - addIssueToContext(ctx, arg); - if (arg.fatal) { - status.abort(); - } else { - status.dirty(); - } - }, - get path() { - return ctx.path; - }, - }; - - checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); if (effect.type === "refinement") { const executeRefinement = ( acc: unknown @@ -4346,7 +4352,7 @@ export class ZodEffects< }; static createWithPreprocess = ( - preprocess: (arg: unknown) => unknown, + preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams ): ZodEffects => {