Skip to content

Commit 36396bb

Browse files
author
Colin McDonnell
committed
Add exact flag to too_big and too_small
1 parent b16fa44 commit 36396bb

File tree

6 files changed

+198
-44
lines changed

6 files changed

+198
-44
lines changed

deno/lib/ZodError.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,15 @@ export interface ZodTooSmallIssue extends ZodIssueBase {
106106
code: typeof ZodIssueCode.too_small;
107107
minimum: number;
108108
inclusive: boolean;
109+
exact: boolean;
109110
type: "array" | "string" | "number" | "set" | "date";
110111
}
111112

112113
export interface ZodTooBigIssue extends ZodIssueBase {
113114
code: typeof ZodIssueCode.too_big;
114115
maximum: number;
115116
inclusive: boolean;
117+
exact: boolean;
116118
type: "array" | "string" | "number" | "set" | "date";
117119
}
118120

deno/lib/locales/en.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -63,39 +63,55 @@ const errorMap: ZodErrorMap = (issue, _ctx) => {
6363
case ZodIssueCode.too_small:
6464
if (issue.type === "array")
6565
message = `Array must contain ${
66-
issue.inclusive ? `at least` : `more than`
66+
issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`
6767
} ${issue.minimum} element(s)`;
6868
else if (issue.type === "string")
6969
message = `String must contain ${
70-
issue.inclusive ? `at least` : `over`
70+
issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`
7171
} ${issue.minimum} character(s)`;
7272
else if (issue.type === "number")
73-
message = `Number must be greater than ${
74-
issue.inclusive ? `or equal to ` : ``
73+
message = `Number must be ${
74+
issue.exact
75+
? `exactly equal to `
76+
: issue.inclusive
77+
? `greater than or equal to `
78+
: `greater than `
7579
}${issue.minimum}`;
7680
else if (issue.type === "date")
77-
message = `Date must be greater than ${
78-
issue.inclusive ? `or equal to ` : ``
81+
message = `Date must be ${
82+
issue.exact
83+
? `exactly equal to `
84+
: issue.inclusive
85+
? `greater than or equal to `
86+
: `greater than `
7987
}${new Date(issue.minimum)}`;
8088
else message = "Invalid input";
8189
break;
8290
case ZodIssueCode.too_big:
8391
if (issue.type === "array")
8492
message = `Array must contain ${
85-
issue.inclusive ? `at most` : `less than`
93+
issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`
8694
} ${issue.maximum} element(s)`;
8795
else if (issue.type === "string")
8896
message = `String must contain ${
89-
issue.inclusive ? `at most` : `under`
97+
issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`
9098
} ${issue.maximum} character(s)`;
9199
else if (issue.type === "number")
92-
message = `Number must be less than ${
93-
issue.inclusive ? `or equal to ` : ``
94-
}${issue.maximum}`;
100+
message = `Number must be ${
101+
issue.exact
102+
? `exactly`
103+
: issue.inclusive
104+
? `less than or equal to`
105+
: `less than`
106+
} ${issue.maximum}`;
95107
else if (issue.type === "date")
96-
message = `Date must be smaller than ${
97-
issue.inclusive ? `or equal to ` : ``
98-
}${new Date(issue.maximum)}`;
108+
message = `Date must be ${
109+
issue.exact
110+
? `exactly`
111+
: issue.inclusive
112+
? `smaller than or equal to`
113+
: `smaller than`
114+
} ${new Date(issue.maximum)}`;
99115
else message = "Invalid input";
100116
break;
101117
case ZodIssueCode.custom:

deno/lib/types.ts

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ export abstract class ZodType<
472472
type ZodStringCheck =
473473
| { kind: "min"; value: number; message?: string }
474474
| { kind: "max"; value: number; message?: string }
475+
| { kind: "length"; value: number; message?: string }
475476
| { kind: "email"; message?: string }
476477
| { kind: "url"; message?: string }
477478
| { kind: "uuid"; message?: string }
@@ -572,6 +573,7 @@ export class ZodString extends ZodType<string, ZodStringDef> {
572573
minimum: check.value,
573574
type: "string",
574575
inclusive: true,
576+
exact: false,
575577
message: check.message,
576578
});
577579
status.dirty();
@@ -584,10 +586,37 @@ export class ZodString extends ZodType<string, ZodStringDef> {
584586
maximum: check.value,
585587
type: "string",
586588
inclusive: true,
589+
exact: false,
587590
message: check.message,
588591
});
589592
status.dirty();
590593
}
594+
} else if (check.kind === "length") {
595+
const tooBig = input.data.length > check.value;
596+
const tooSmall = input.data.length < check.value;
597+
if (tooBig || tooSmall) {
598+
ctx = this._getOrReturnCtx(input, ctx);
599+
if (tooBig) {
600+
addIssueToContext(ctx, {
601+
code: ZodIssueCode.too_big,
602+
maximum: check.value,
603+
type: "string",
604+
inclusive: true,
605+
exact: true,
606+
message: check.message,
607+
});
608+
} else if (tooSmall) {
609+
addIssueToContext(ctx, {
610+
code: ZodIssueCode.too_small,
611+
minimum: check.value,
612+
type: "string",
613+
inclusive: true,
614+
exact: true,
615+
message: check.message,
616+
});
617+
}
618+
status.dirty();
619+
}
591620
} else if (check.kind === "email") {
592621
if (!emailRegex.test(input.data)) {
593622
ctx = this._getOrReturnCtx(input, ctx);
@@ -781,10 +810,11 @@ export class ZodString extends ZodType<string, ZodStringDef> {
781810
}
782811

783812
length(len: number, message?: errorUtil.ErrMessage) {
784-
const defaultMessage = `String must contain exactly ${len} character(s)`;
785-
const actualMessage = message ?? defaultMessage;
786-
787-
return this.min(len, actualMessage).max(len, actualMessage);
813+
return this._addCheck({
814+
kind: "length",
815+
value: len,
816+
...errorUtil.errToObj(message),
817+
});
788818
}
789819

790820
/**
@@ -913,6 +943,7 @@ export class ZodNumber extends ZodType<number, ZodNumberDef> {
913943
minimum: check.value,
914944
type: "number",
915945
inclusive: check.inclusive,
946+
exact: false,
916947
message: check.message,
917948
});
918949
status.dirty();
@@ -928,6 +959,7 @@ export class ZodNumber extends ZodType<number, ZodNumberDef> {
928959
maximum: check.value,
929960
type: "number",
930961
inclusive: check.inclusive,
962+
exact: false,
931963
message: check.message,
932964
});
933965
status.dirty();
@@ -1214,6 +1246,7 @@ export class ZodDate extends ZodType<Date, ZodDateDef> {
12141246
code: ZodIssueCode.too_small,
12151247
message: check.message,
12161248
inclusive: true,
1249+
exact: false,
12171250
minimum: check.value,
12181251
type: "date",
12191252
});
@@ -1226,6 +1259,7 @@ export class ZodDate extends ZodType<Date, ZodDateDef> {
12261259
code: ZodIssueCode.too_big,
12271260
message: check.message,
12281261
inclusive: true,
1262+
exact: false,
12291263
maximum: check.value,
12301264
type: "date",
12311265
});
@@ -1524,6 +1558,7 @@ export interface ZodArrayDef<T extends ZodTypeAny = ZodTypeAny>
15241558
extends ZodTypeDef {
15251559
type: T;
15261560
typeName: ZodFirstPartyTypeKind.ZodArray;
1561+
exactLength: { value: number; message?: string } | null;
15271562
minLength: { value: number; message?: string } | null;
15281563
maxLength: { value: number; message?: string } | null;
15291564
}
@@ -1560,13 +1595,31 @@ export class ZodArray<
15601595
return INVALID;
15611596
}
15621597

1598+
if (def.exactLength !== null) {
1599+
const tooBig = ctx.data.length > def.exactLength.value;
1600+
const tooSmall = ctx.data.length < def.exactLength.value;
1601+
if (tooBig || tooSmall) {
1602+
addIssueToContext(ctx, {
1603+
code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small,
1604+
minimum: (tooSmall ? def.exactLength.value : undefined) as number,
1605+
maximum: (tooBig ? def.exactLength.value : undefined) as number,
1606+
type: "array",
1607+
inclusive: true,
1608+
exact: true,
1609+
message: def.exactLength.message,
1610+
});
1611+
status.dirty();
1612+
}
1613+
}
1614+
15631615
if (def.minLength !== null) {
15641616
if (ctx.data.length < def.minLength.value) {
15651617
addIssueToContext(ctx, {
15661618
code: ZodIssueCode.too_small,
15671619
minimum: def.minLength.value,
15681620
type: "array",
15691621
inclusive: true,
1622+
exact: false,
15701623
message: def.minLength.message,
15711624
});
15721625
status.dirty();
@@ -1580,6 +1633,7 @@ export class ZodArray<
15801633
maximum: def.maxLength.value,
15811634
type: "array",
15821635
inclusive: true,
1636+
exact: false,
15831637
message: def.maxLength.message,
15841638
});
15851639
status.dirty();
@@ -1626,10 +1680,10 @@ export class ZodArray<
16261680
}
16271681

16281682
length(len: number, message?: errorUtil.ErrMessage): this {
1629-
const defaultMessage = `Array must contain exactly ${len} element(s)`;
1630-
const actualMessage = message ?? defaultMessage;
1631-
1632-
return this.min(len, actualMessage).max(len, actualMessage) as any;
1683+
return new ZodArray({
1684+
...this._def,
1685+
exactLength: { value: len, message: errorUtil.toString(message) },
1686+
}) as any;
16331687
}
16341688

16351689
nonempty(message?: errorUtil.ErrMessage): ZodArray<T, "atleastone"> {
@@ -1644,6 +1698,7 @@ export class ZodArray<
16441698
type: schema,
16451699
minLength: null,
16461700
maxLength: null,
1701+
exactLength: null,
16471702
typeName: ZodFirstPartyTypeKind.ZodArray,
16481703
...processCreateParams(params),
16491704
});
@@ -2706,6 +2761,7 @@ export class ZodTuple<
27062761
code: ZodIssueCode.too_small,
27072762
minimum: this._def.items.length,
27082763
inclusive: true,
2764+
exact: false,
27092765
type: "array",
27102766
});
27112767

@@ -2719,6 +2775,7 @@ export class ZodTuple<
27192775
code: ZodIssueCode.too_big,
27202776
maximum: this._def.items.length,
27212777
inclusive: true,
2778+
exact: false,
27222779
type: "array",
27232780
});
27242781
status.dirty();
@@ -3017,6 +3074,7 @@ export class ZodSet<Value extends ZodTypeAny = ZodTypeAny> extends ZodType<
30173074
minimum: def.minSize.value,
30183075
type: "set",
30193076
inclusive: true,
3077+
exact: false,
30203078
message: def.minSize.message,
30213079
});
30223080
status.dirty();
@@ -3030,6 +3088,7 @@ export class ZodSet<Value extends ZodTypeAny = ZodTypeAny> extends ZodType<
30303088
maximum: def.maxSize.value,
30313089
type: "set",
30323090
inclusive: true,
3091+
exact: false,
30333092
message: def.maxSize.message,
30343093
});
30353094
status.dirty();

src/ZodError.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,15 @@ export interface ZodTooSmallIssue extends ZodIssueBase {
106106
code: typeof ZodIssueCode.too_small;
107107
minimum: number;
108108
inclusive: boolean;
109+
exact: boolean;
109110
type: "array" | "string" | "number" | "set" | "date";
110111
}
111112

112113
export interface ZodTooBigIssue extends ZodIssueBase {
113114
code: typeof ZodIssueCode.too_big;
114115
maximum: number;
115116
inclusive: boolean;
117+
exact: boolean;
116118
type: "array" | "string" | "number" | "set" | "date";
117119
}
118120

src/locales/en.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -63,39 +63,55 @@ const errorMap: ZodErrorMap = (issue, _ctx) => {
6363
case ZodIssueCode.too_small:
6464
if (issue.type === "array")
6565
message = `Array must contain ${
66-
issue.inclusive ? `at least` : `more than`
66+
issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`
6767
} ${issue.minimum} element(s)`;
6868
else if (issue.type === "string")
6969
message = `String must contain ${
70-
issue.inclusive ? `at least` : `over`
70+
issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`
7171
} ${issue.minimum} character(s)`;
7272
else if (issue.type === "number")
73-
message = `Number must be greater than ${
74-
issue.inclusive ? `or equal to ` : ``
73+
message = `Number must be ${
74+
issue.exact
75+
? `exactly equal to `
76+
: issue.inclusive
77+
? `greater than or equal to `
78+
: `greater than `
7579
}${issue.minimum}`;
7680
else if (issue.type === "date")
77-
message = `Date must be greater than ${
78-
issue.inclusive ? `or equal to ` : ``
81+
message = `Date must be ${
82+
issue.exact
83+
? `exactly equal to `
84+
: issue.inclusive
85+
? `greater than or equal to `
86+
: `greater than `
7987
}${new Date(issue.minimum)}`;
8088
else message = "Invalid input";
8189
break;
8290
case ZodIssueCode.too_big:
8391
if (issue.type === "array")
8492
message = `Array must contain ${
85-
issue.inclusive ? `at most` : `less than`
93+
issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`
8694
} ${issue.maximum} element(s)`;
8795
else if (issue.type === "string")
8896
message = `String must contain ${
89-
issue.inclusive ? `at most` : `under`
97+
issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`
9098
} ${issue.maximum} character(s)`;
9199
else if (issue.type === "number")
92-
message = `Number must be less than ${
93-
issue.inclusive ? `or equal to ` : ``
94-
}${issue.maximum}`;
100+
message = `Number must be ${
101+
issue.exact
102+
? `exactly`
103+
: issue.inclusive
104+
? `less than or equal to`
105+
: `less than`
106+
} ${issue.maximum}`;
95107
else if (issue.type === "date")
96-
message = `Date must be smaller than ${
97-
issue.inclusive ? `or equal to ` : ``
98-
}${new Date(issue.maximum)}`;
108+
message = `Date must be ${
109+
issue.exact
110+
? `exactly`
111+
: issue.inclusive
112+
? `smaller than or equal to`
113+
: `smaller than`
114+
} ${new Date(issue.maximum)}`;
99115
else message = "Invalid input";
100116
break;
101117
case ZodIssueCode.custom:

0 commit comments

Comments
 (0)