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

Reporting errors of the preprocess that is the second property of object #2912

Merged
merged 7 commits into from
Mar 21, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions deno/lib/ZodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,12 @@ export class ZodError<T = any> extends Error {
return error;
};

static assert(value: unknown): asserts value is ZodError {
if (!(value instanceof ZodError)) {
throw new Error(`Not a ZodError: ${value}`);
}
}

toString() {
return this.message;
}
Expand Down
193 changes: 193 additions & 0 deletions deno/lib/__tests__/preprocess.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// @ts-ignore TS6133
import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts";
const test = Deno.test;

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

test("preprocess", () => {
const schema = z.preprocess((data) => [data], z.string().array());

const value = schema.parse("asdf");
expect(value).toEqual(["asdf"]);
util.assertEqual<(typeof schema)["_input"], unknown>(true);
});

test("async preprocess", async () => {
const schema = z.preprocess(async (data) => [data], z.string().array());

const value = await schema.parseAsync("asdf");
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 non-fatal by default", () => {

try{
z.preprocess((data, ctx) => {
ctx.addIssue({
code: "custom",
message: `custom error`,
});
return data;
}, z.string()).parse(1234);
}catch(err){
z.ZodError.assert(err);
expect(err.issues.length).toEqual(2);
}
})

test("preprocess ctx.addIssue fatal true", () => {
try{
z.preprocess((data, ctx) => {
ctx.addIssue({
code: "custom",
message: `custom error`,
fatal: true
});
return data;
}, z.string()).parse(1234);
}catch(err){
z.ZodError.assert(err);
expect(err.issues.length).toEqual(1);
}
});

test("async preprocess ctx.addIssue with parse", async () => {
const schema = z.preprocess(async (data, ctx) => {
ctx.addIssue({
code: "custom",
message: `custom error`,
});
return data;
}, z.string());

expect(schema.parseAsync("asdf")).rejects.toThrow(
JSON.stringify(
[
{
code: "custom",
message: "custom error",
path: [],
},
],
null,
2
)
);
});

test("preprocess ctx.addIssue with parseAsync", async () => {
const result = await z
.preprocess(async (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", () => {
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<typeof foo>;
util.assertEqual<foo, number>(true);
const arg = foo.safeParse(undefined);
if (!arg.success) {
expect(arg.error.issues[0].message).toEqual("bad");
}
});
test("preprocess as the second property of object", () => {
const schema = z.object({
nonEmptyStr: z.string().min(1),
positiveNum: z.preprocess((v) => Number(v), z.number().positive()),
});
const result = schema.safeParse({
nonEmptyStr: "",
positiveNum: "",
});
expect(result.success).toEqual(false);
if (!result.success) {
expect(result.error.issues.length).toEqual(2);
expect(result.error.issues[0].code).toEqual(z.ZodIssueCode.too_small);
expect(result.error.issues[1].code).toEqual(z.ZodIssueCode.too_small);
}
});

test("preprocess validates with sibling errors", () => {
expect(() => {
z.object({
// Must be first
missing: z.string().refine(() => false),
preprocess: z.preprocess(
(data: any) => data?.trim(),
z.string().regex(/ asdf/)
),
}).parse({ preprocess: " asdf" });
}).toThrow(
JSON.stringify(
[
{
code: "invalid_type",
expected: "string",
received: "undefined",
path: ["missing"],
message: "Required",
},
{
validation: "regex",
code: "invalid_string",
message: "Invalid",
path: ["preprocess"],
},
],
null,
2
)
);
});
85 changes: 3 additions & 82 deletions deno/lib/__tests__/transformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ test("transform ctx.addIssue with parseAsync", async () => {

const result = await z
.string()
.transform((data, ctx) => {
.transform(async (data, ctx) => {
const i = strs.indexOf(data);
if (i === -1) {
ctx.addIssue({
Expand All @@ -74,7 +74,7 @@ test("transform ctx.addIssue with parseAsync", async () => {
});
});

test("z.NEVER in transform", async () => {
test("z.NEVER in transform", () => {
const foo = z
.number()
.optional()
Expand Down Expand Up @@ -197,87 +197,7 @@ test("multiple transformers", () => {
expect(doubler.parse("5")).toEqual(10);
});

test("preprocess", () => {
const schema = z.preprocess((data) => [data], z.string().array());

const value = schema.parse("asdf");
expect(value).toEqual(["asdf"]);
util.assertEqual<(typeof schema)["_input"], unknown>(true);
});

test("async preprocess", async () => {
const schema = z.preprocess(async (data) => [data], z.string().array());

const value = await schema.parseAsync("asdf");
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<typeof foo>;
util.assertEqual<foo, number>(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
Expand All @@ -297,6 +217,7 @@ test("short circuit on dirty", () => {
}
});


test("async short circuit on dirty", async () => {
const schema = z
.string()
Expand Down
2 changes: 1 addition & 1 deletion deno/lib/helpers/parseUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export const isAborted = (x: ParseReturnType<any>): x is INVALID =>
(x as any).status === "aborted";
export const isDirty = <T>(x: ParseReturnType<T>): x is OK<T> | DIRTY<T> =>
(x as any).status === "dirty";
export const isValid = <T>(x: ParseReturnType<T>): x is OK<T> | DIRTY<T> =>
export const isValid = <T>(x: ParseReturnType<T>): x is OK<T> =>
(x as any).status === "valid";
export const isAsync = <T>(
x: ParseReturnType<T>
Expand Down
28 changes: 15 additions & 13 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4291,34 +4291,36 @@ export class ZodEffects<

if (effect.type === "preprocess") {
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) => {
return this._def.schema._parseAsync({
return Promise.resolve(processed).then(async (processed) => {
if (status.value === "aborted") return INVALID;

const result = await this._def.schema._parseAsync({
data: processed,
path: ctx.path,
parent: ctx,
});
if (result.status === "aborted") return INVALID;
if (result.status === "dirty") return DIRTY(result.value);
if (status.value === "dirty") return DIRTY(result.value);
return result;
});
} else {
return this._def.schema._parseSync({
if (status.value === "aborted") return INVALID;
const result = this._def.schema._parseSync({
data: processed,
path: ctx.path,
parent: ctx,
});
if (result.status === "aborted") return INVALID;
if (result.status === "dirty") return DIRTY(result.value);
if (status.value === "dirty") return DIRTY(result.value);
return result;
}
}
if (effect.type === "refinement") {
const executeRefinement = (
acc: unknown
// effect: RefinementEffect<any>
): any => {
const executeRefinement = (acc: unknown): any => {
const result = effect.refinement(acc, checkCtx);
if (ctx.common.async) {
return Promise.resolve(result);
Expand Down
6 changes: 0 additions & 6 deletions playground.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
import { z } from "./src";

z;

function dataObj<T extends z.ZodTypeAny>(schema: T) {
return z.object({ data: schema as T }).transform((a: T['_output']) => a.data);
}

const arg= dataObj(z.string()).parse({ data: "asdf" });
Loading
Loading