Skip to content

Commit

Permalink
Fix issue #1611 (#1620)
Browse files Browse the repository at this point in the history
* Add exact length message for arrays

* Add custom validation message for z.string().length()

* Add exact flag to too_big and too_small

Co-authored-by: Colin McDonnell <[email protected]>
  • Loading branch information
john-schmitz and Colin McDonnell authored Dec 12, 2022
1 parent 497d44b commit 9828837
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 32 deletions.
2 changes: 2 additions & 0 deletions deno/lib/ZodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,15 @@ export interface ZodTooSmallIssue extends ZodIssueBase {
code: typeof ZodIssueCode.too_small;
minimum: number;
inclusive: boolean;
exact: boolean;
type: "array" | "string" | "number" | "set" | "date";
}

export interface ZodTooBigIssue extends ZodIssueBase {
code: typeof ZodIssueCode.too_big;
maximum: number;
inclusive: boolean;
exact: boolean;
type: "array" | "string" | "number" | "set" | "date";
}

Expand Down
36 changes: 36 additions & 0 deletions deno/lib/__tests__/validations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,42 @@ test("array max", async () => {
}
});

test("array length", async () => {
try {
await z.array(z.string()).length(2).parseAsync(["asdf", "asdf", "asdf"]);
} catch (err) {
expect((err as z.ZodError).issues[0].message).toEqual(
"Array must contain exactly 2 element(s)"
);
}

try {
await z.array(z.string()).length(2).parseAsync(["asdf"]);
} catch (err) {
expect((err as z.ZodError).issues[0].message).toEqual(
"Array must contain exactly 2 element(s)"
);
}
});

test("string length", async () => {
try {
await z.string().length(4).parseAsync("asd");
} catch (err) {
expect((err as z.ZodError).issues[0].message).toEqual(
"String must contain exactly 4 character(s)"
);
}

try {
await z.string().length(4).parseAsync("asdaa");
} catch (err) {
expect((err as z.ZodError).issues[0].message).toEqual(
"String must contain exactly 4 character(s)"
);
}
});

test("string min", async () => {
try {
await z.string().min(4).parseAsync("asd");
Expand Down
44 changes: 30 additions & 14 deletions deno/lib/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,39 +63,55 @@ const errorMap: ZodErrorMap = (issue, _ctx) => {
case ZodIssueCode.too_small:
if (issue.type === "array")
message = `Array must contain ${
issue.inclusive ? `at least` : `more than`
issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`
} ${issue.minimum} element(s)`;
else if (issue.type === "string")
message = `String must contain ${
issue.inclusive ? `at least` : `over`
issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`
} ${issue.minimum} character(s)`;
else if (issue.type === "number")
message = `Number must be greater than ${
issue.inclusive ? `or equal to ` : ``
message = `Number must be ${
issue.exact
? `exactly equal to `
: issue.inclusive
? `greater than or equal to `
: `greater than `
}${issue.minimum}`;
else if (issue.type === "date")
message = `Date must be greater than ${
issue.inclusive ? `or equal to ` : ``
message = `Date must be ${
issue.exact
? `exactly equal to `
: issue.inclusive
? `greater than or equal to `
: `greater than `
}${new Date(issue.minimum)}`;
else message = "Invalid input";
break;
case ZodIssueCode.too_big:
if (issue.type === "array")
message = `Array must contain ${
issue.inclusive ? `at most` : `less than`
issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`
} ${issue.maximum} element(s)`;
else if (issue.type === "string")
message = `String must contain ${
issue.inclusive ? `at most` : `under`
issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`
} ${issue.maximum} character(s)`;
else if (issue.type === "number")
message = `Number must be less than ${
issue.inclusive ? `or equal to ` : ``
}${issue.maximum}`;
message = `Number must be ${
issue.exact
? `exactly`
: issue.inclusive
? `less than or equal to`
: `less than`
} ${issue.maximum}`;
else if (issue.type === "date")
message = `Date must be smaller than ${
issue.inclusive ? `or equal to ` : ``
}${new Date(issue.maximum)}`;
message = `Date must be ${
issue.exact
? `exactly`
: issue.inclusive
? `smaller than or equal to`
: `smaller than`
} ${new Date(issue.maximum)}`;
else message = "Invalid input";
break;
case ZodIssueCode.custom:
Expand Down
69 changes: 67 additions & 2 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ export abstract class ZodType<
export type ZodStringCheck =
| { kind: "min"; value: number; message?: string }
| { kind: "max"; value: number; message?: string }
| { kind: "length"; value: number; message?: string }
| { kind: "email"; message?: string }
| { kind: "url"; message?: string }
| { kind: "uuid"; message?: string }
Expand Down Expand Up @@ -589,6 +590,7 @@ export class ZodString extends ZodType<string, ZodStringDef> {
minimum: check.value,
type: "string",
inclusive: true,
exact: false,
message: check.message,
});
status.dirty();
Expand All @@ -601,10 +603,37 @@ export class ZodString extends ZodType<string, ZodStringDef> {
maximum: check.value,
type: "string",
inclusive: true,
exact: false,
message: check.message,
});
status.dirty();
}
} else if (check.kind === "length") {
const tooBig = input.data.length > check.value;
const tooSmall = input.data.length < check.value;
if (tooBig || tooSmall) {
ctx = this._getOrReturnCtx(input, ctx);
if (tooBig) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_big,
maximum: check.value,
type: "string",
inclusive: true,
exact: true,
message: check.message,
});
} else if (tooSmall) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_small,
minimum: check.value,
type: "string",
inclusive: true,
exact: true,
message: check.message,
});
}
status.dirty();
}
} else if (check.kind === "email") {
if (!emailRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
Expand Down Expand Up @@ -798,7 +827,11 @@ export class ZodString extends ZodType<string, ZodStringDef> {
}

length(len: number, message?: errorUtil.ErrMessage) {
return this.min(len, message).max(len, message);
return this._addCheck({
kind: "length",
value: len,
...errorUtil.errToObj(message),
});
}

/**
Expand Down Expand Up @@ -932,6 +965,7 @@ export class ZodNumber extends ZodType<number, ZodNumberDef> {
minimum: check.value,
type: "number",
inclusive: check.inclusive,
exact: false,
message: check.message,
});
status.dirty();
Expand All @@ -947,6 +981,7 @@ export class ZodNumber extends ZodType<number, ZodNumberDef> {
maximum: check.value,
type: "number",
inclusive: check.inclusive,
exact: false,
message: check.message,
});
status.dirty();
Expand Down Expand Up @@ -1255,6 +1290,7 @@ export class ZodDate extends ZodType<Date, ZodDateDef> {
code: ZodIssueCode.too_small,
message: check.message,
inclusive: true,
exact: false,
minimum: check.value,
type: "date",
});
Expand All @@ -1267,6 +1303,7 @@ export class ZodDate extends ZodType<Date, ZodDateDef> {
code: ZodIssueCode.too_big,
message: check.message,
inclusive: true,
exact: false,
maximum: check.value,
type: "date",
});
Expand Down Expand Up @@ -1568,6 +1605,7 @@ export interface ZodArrayDef<T extends ZodTypeAny = ZodTypeAny>
extends ZodTypeDef {
type: T;
typeName: ZodFirstPartyTypeKind.ZodArray;
exactLength: { value: number; message?: string } | null;
minLength: { value: number; message?: string } | null;
maxLength: { value: number; message?: string } | null;
}
Expand Down Expand Up @@ -1604,13 +1642,31 @@ export class ZodArray<
return INVALID;
}

if (def.exactLength !== null) {
const tooBig = ctx.data.length > def.exactLength.value;
const tooSmall = ctx.data.length < def.exactLength.value;
if (tooBig || tooSmall) {
addIssueToContext(ctx, {
code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small,
minimum: (tooSmall ? def.exactLength.value : undefined) as number,
maximum: (tooBig ? def.exactLength.value : undefined) as number,
type: "array",
inclusive: true,
exact: true,
message: def.exactLength.message,
});
status.dirty();
}
}

if (def.minLength !== null) {
if (ctx.data.length < def.minLength.value) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_small,
minimum: def.minLength.value,
type: "array",
inclusive: true,
exact: false,
message: def.minLength.message,
});
status.dirty();
Expand All @@ -1624,6 +1680,7 @@ export class ZodArray<
maximum: def.maxLength.value,
type: "array",
inclusive: true,
exact: false,
message: def.maxLength.message,
});
status.dirty();
Expand Down Expand Up @@ -1670,7 +1727,10 @@ export class ZodArray<
}

length(len: number, message?: errorUtil.ErrMessage): this {
return this.min(len, message).max(len, message) as any;
return new ZodArray({
...this._def,
exactLength: { value: len, message: errorUtil.toString(message) },
}) as any;
}

nonempty(message?: errorUtil.ErrMessage): ZodArray<T, "atleastone"> {
Expand All @@ -1685,6 +1745,7 @@ export class ZodArray<
type: schema,
minLength: null,
maxLength: null,
exactLength: null,
typeName: ZodFirstPartyTypeKind.ZodArray,
...processCreateParams(params),
});
Expand Down Expand Up @@ -2747,6 +2808,7 @@ export class ZodTuple<
code: ZodIssueCode.too_small,
minimum: this._def.items.length,
inclusive: true,
exact: false,
type: "array",
});

Expand All @@ -2760,6 +2822,7 @@ export class ZodTuple<
code: ZodIssueCode.too_big,
maximum: this._def.items.length,
inclusive: true,
exact: false,
type: "array",
});
status.dirty();
Expand Down Expand Up @@ -3060,6 +3123,7 @@ export class ZodSet<Value extends ZodTypeAny = ZodTypeAny> extends ZodType<
minimum: def.minSize.value,
type: "set",
inclusive: true,
exact: false,
message: def.minSize.message,
});
status.dirty();
Expand All @@ -3073,6 +3137,7 @@ export class ZodSet<Value extends ZodTypeAny = ZodTypeAny> extends ZodType<
maximum: def.maxSize.value,
type: "set",
inclusive: true,
exact: false,
message: def.maxSize.message,
});
status.dirty();
Expand Down
2 changes: 2 additions & 0 deletions src/ZodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,15 @@ export interface ZodTooSmallIssue extends ZodIssueBase {
code: typeof ZodIssueCode.too_small;
minimum: number;
inclusive: boolean;
exact: boolean;
type: "array" | "string" | "number" | "set" | "date";
}

export interface ZodTooBigIssue extends ZodIssueBase {
code: typeof ZodIssueCode.too_big;
maximum: number;
inclusive: boolean;
exact: boolean;
type: "array" | "string" | "number" | "set" | "date";
}

Expand Down
36 changes: 36 additions & 0 deletions src/__tests__/validations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,42 @@ test("array max", async () => {
}
});

test("array length", async () => {
try {
await z.array(z.string()).length(2).parseAsync(["asdf", "asdf", "asdf"]);
} catch (err) {
expect((err as z.ZodError).issues[0].message).toEqual(
"Array must contain exactly 2 element(s)"
);
}

try {
await z.array(z.string()).length(2).parseAsync(["asdf"]);
} catch (err) {
expect((err as z.ZodError).issues[0].message).toEqual(
"Array must contain exactly 2 element(s)"
);
}
});

test("string length", async () => {
try {
await z.string().length(4).parseAsync("asd");
} catch (err) {
expect((err as z.ZodError).issues[0].message).toEqual(
"String must contain exactly 4 character(s)"
);
}

try {
await z.string().length(4).parseAsync("asdaa");
} catch (err) {
expect((err as z.ZodError).issues[0].message).toEqual(
"String must contain exactly 4 character(s)"
);
}
});

test("string min", async () => {
try {
await z.string().min(4).parseAsync("asd");
Expand Down
Loading

0 comments on commit 9828837

Please sign in to comment.