diff --git a/src/zod/index.ts b/src/zod/index.ts index a88f442..53dd840 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -1 +1,2 @@ export { partialSafeParse, PartialSafeParseResult, RecursivePartial } from './partial-safe-parse.js'; +export { StringyIntegerSchema } from './stringy-integer-schema.js'; diff --git a/src/zod/stringy-integer-schema.ts b/src/zod/stringy-integer-schema.ts new file mode 100644 index 0000000..a0f8e54 --- /dev/null +++ b/src/zod/stringy-integer-schema.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +/** + * This validates the string is a plain non-negative integer and converts it to a `number`. + * - Also rejects values that would lose precision past `Number.MAX_SAFE_INTEGER` + */ +export const StringyIntegerSchema = z + .string() + .regex(/^\d+$/, { + error: (issue) => `Invalid input: expected numeric string, received ${JSON.stringify(issue.input)}`, + }) + .transform((value, ctx) => { + const number = Number(value); + if (Number.isSafeInteger(number)) return number; + + ctx.addIssue({ + code: 'custom', + message: + `Invalid input: stringy integer "${value}" exceeds Number.MAX_SAFE_INTEGER and would lose precision` + + ' - use z.string() for this field instead', + }); + return z.NEVER; + }); diff --git a/test/zod/stringy-integer-schema.test.ts b/test/zod/stringy-integer-schema.test.ts new file mode 100644 index 0000000..a1d23c8 --- /dev/null +++ b/test/zod/stringy-integer-schema.test.ts @@ -0,0 +1,34 @@ +import { assert, describe, expect, it } from 'vitest'; + +import { StringyIntegerSchema } from '../../src/zod/stringy-integer-schema.js'; + +describe('StringyIntegerSchema', () => { + it('should validate and transform stringy integers correctly', () => { + // Act + const result = StringyIntegerSchema.parse('123'); + + // Assert + expect(result).toBe(123); + }); + + it('should throw an error for invalid stringy integers', () => { + // Act + const result = StringyIntegerSchema.safeParse('abc'); + + // Assert + assert(result.error); + expect(result.error.issues[0].message).toStrictEqual('Invalid input: expected numeric string, received "abc"'); + }); + + it('should throw an error for number over the maximum safe integer', () => { + // Act + const result = StringyIntegerSchema.safeParse('9007199254740992'); // Number.MAX_SAFE_INTEGER + 1 + + // Assert + assert(result.error); + expect(result.error.issues[0].message).toStrictEqual( + 'Invalid input: stringy integer "9007199254740992" exceeds Number.MAX_SAFE_INTEGER and would lose ' + + 'precision - use z.string() for this field instead', + ); + }); +});