Skip to content
Open
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
8 changes: 8 additions & 0 deletions packages/zod/src/v4/classic/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1434,12 +1434,20 @@ export interface ZodMap<Key extends core.SomeType = core.$ZodType, Value extends
core.$ZodMap<Key, Value> {
keyType: Key;
valueType: Value;
min(minSize: number, params?: string | core.$ZodCheckMinSizeParams): this;
nonempty(params?: string | core.$ZodCheckMinSizeParams): this;
max(maxSize: number, params?: string | core.$ZodCheckMaxSizeParams): this;
size(size: number, params?: string | core.$ZodCheckSizeEqualsParams): this;
}
export const ZodMap: core.$constructor<ZodMap> = /*@__PURE__*/ core.$constructor("ZodMap", (inst, def) => {
core.$ZodMap.init(inst, def);
ZodType.init(inst, def);
inst.keyType = def.keyType;
inst.valueType = def.valueType;
inst.min = (...args) => inst.check(core._minSize(...args));
inst.nonempty = (params) => inst.check(core._minSize(1, params));
inst.max = (...args) => inst.check(core._maxSize(...args));
inst.size = (...args) => inst.check(core._size(...args));
});

export function map<Key extends core.SomeType, Value extends core.SomeType>(
Expand Down
134 changes: 134 additions & 0 deletions packages/zod/src/v4/classic/tests/map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import * as z from "zod/v4";
const stringMap = z.map(z.string(), z.string());
type stringMap = z.infer<typeof stringMap>;

const minTwo = stringMap.min(2);
const maxTwo = stringMap.max(2);
const justTwo = stringMap.size(2);
const nonEmpty = stringMap.nonempty();
const nonEmptyMax = stringMap.nonempty().max(2);

test("type inference", () => {
expectTypeOf<stringMap>().toEqualTypeOf<Map<string, string>>();
});
Expand All @@ -24,6 +30,75 @@ test("valid parse", () => {
`);
});

test("valid parse: size-related methods", () => {
expect(() => {
minTwo.parse(
new Map([
["a", "b"],
["c", "d"],
])
);
minTwo.parse(
new Map([
["a", "b"],
["c", "d"],
["e", "f"],
])
);
maxTwo.parse(
new Map([
["a", "b"],
["c", "d"],
])
);
maxTwo.parse(new Map([["a", "b"]]));
justTwo.parse(
new Map([
["a", "b"],
["c", "d"],
])
);
nonEmpty.parse(new Map([["a", "b"]]));
nonEmptyMax.parse(
new Map([
["a", "b"],
["c", "d"],
])
);
}).not.toThrow();

const sizeZeroResult = stringMap.parse(new Map());
expect(sizeZeroResult.size).toBe(0);

const sizeTwoResult = minTwo.parse(
new Map([
["a", "b"],
["c", "d"],
])
);
expect(sizeTwoResult.size).toBe(2);
});

test("failing when parsing empty map in nonempty ", () => {
const result = nonEmpty.safeParse(new Map());
expect(result.success).toEqual(false);
expect(result.error!.issues.length).toEqual(1);
expect(result.error!.issues[0].code).toEqual("too_small");
});

test("failing when map is bigger than max() ", () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know if the trailing spaces in the test titles were intentional. I just copied them from set.test.ts.

const result = maxTwo.safeParse(
new Map([
["a", "b"],
["c", "d"],
["e", "f"],
])
);
expect(result.success).toEqual(false);
expect(result.error!.issues.length).toEqual(1);
expect(result.error!.issues[0].code).toEqual("too_big");
});

test("valid parse async", async () => {
const asyncMap = z.map(
z.string().refine(async () => false, "bad key"),
Expand Down Expand Up @@ -194,3 +269,62 @@ test("map with object keys", () => {
]]
`);
});

test("min/max", async () => {
const schema = stringMap.min(4).max(5);

const r1 = schema.safeParse(
new Map([
["a", "a"],
["b", "b"],
["c", "c"],
["d", "d"],
])
);
expect(r1.success).toEqual(true);

const r2 = schema.safeParse(
new Map([
["a", "a"],
["b", "b"],
["c", "c"],
])
);
expect(r2.success).toEqual(false);
expect(r2.error!.issues).toMatchInlineSnapshot(`
[
{
"code": "too_small",
"inclusive": true,
"message": "Too small: expected map to have >=4 entries",
"minimum": 4,
"origin": "map",
"path": [],
},
]
`);

const r3 = schema.safeParse(
new Map([
["a", "a"],
["b", "b"],
["c", "c"],
["d", "d"],
["e", "e"],
["f", "f"],
])
);
expect(r3.success).toEqual(false);
expect(r3.error!.issues).toMatchInlineSnapshot(`
[
{
"code": "too_big",
"inclusive": true,
"maximum": 5,
"message": "Too big: expected map to have <=5 entries",
"origin": "map",
"path": [],
},
]
`);
});
1 change: 1 addition & 0 deletions packages/zod/src/v4/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const error: () => errors.$ZodErrorMap = () => {
file: { unit: "bytes", verb: "to have" },
array: { unit: "items", verb: "to have" },
set: { unit: "items", verb: "to have" },
map: { unit: "entries", verb: "to have" },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured "entries" was appropriate. I'm not sure if I need to apply this to the rest of the locale files

};

function getSizing(origin: string): { unit: string; verb: string } | null {
Expand Down