zod-error-map 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # zod-error-map
2
+
3
+ Type-safe, customizable error message mapping for Zod validation.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install zod-error-map
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Basic Usage (Zod v4)
14
+
15
+ ```js
16
+ import { z } from 'zod'
17
+ import { setZodErrorMap } from 'zod-error-map'
18
+
19
+ setZodErrorMap(z)
20
+
21
+ const schema = z.object({
22
+ email: z.string().email(),
23
+ password: z.string().min(8),
24
+ })
25
+
26
+ schema.parse({ email: 'invalid', password: '123' })
27
+ // Error: The 'email' must be a valid email address
28
+ ```
29
+
30
+ ### Custom Configuration
31
+
32
+ ```js
33
+ import { z } from 'zod'
34
+ import { setZodErrorMap, ErrorCode } from 'zod-error-map'
35
+
36
+ setZodErrorMap(z, {
37
+ defaultError: 'Validation failed',
38
+ formatMessages: {
39
+ email: (label) => `Please enter a valid email for ${label.quoted}`,
40
+ },
41
+ builders: {
42
+ [ErrorCode.TOO_SMALL]: (issue, label) =>
43
+ `${label.bare} needs at least ${issue.minimum} characters`,
44
+ },
45
+ })
46
+ ```
47
+
48
+ ### Using the Error Mapper Directly
49
+
50
+ ```js
51
+ import { createErrorMapper } from 'zod-error-map'
52
+
53
+ const mapper = createErrorMapper({
54
+ defaultError: 'Invalid input',
55
+ })
56
+
57
+ const message = mapper.format({
58
+ code: 'invalid_type',
59
+ path: ['email'],
60
+ input: undefined,
61
+ expected: 'string',
62
+ })
63
+ // "The email is required"
64
+ ```
65
+
66
+ ### Zod v3 Compatibility
67
+
68
+ ```js
69
+ import { z } from 'zod'
70
+ import { createZodErrorMap } from 'zod-error-map'
71
+
72
+ z.setErrorMap(createZodErrorMap())
73
+ ```
74
+
75
+ ## API
76
+
77
+ ### `setZodErrorMap(z, config?)`
78
+
79
+ Sets the global Zod error map using `z.config()` (Zod v4).
80
+
81
+ ### `createZodErrorMap(config?)`
82
+
83
+ Creates a Zod error map compatible with `z.setErrorMap()` (Zod v3).
84
+
85
+ ### `createErrorMapper(config?)`
86
+
87
+ Creates an error mapper instance with custom configuration.
88
+
89
+ ### Configuration Options
90
+
91
+ | Option | Type | Description |
92
+ |--------|------|-------------|
93
+ | `defaultError` | `string` | Default error message when no builder matches |
94
+ | `builders` | `Record<string, MessageBuilder>` | Custom message builders by error code |
95
+ | `formatMessages` | `Record<string, (label) => string>` | Custom messages for format validation errors |
96
+
97
+ ### Error Codes
98
+
99
+ - `ErrorCode.INVALID_TYPE` - Type mismatch or missing required field
100
+ - `ErrorCode.TOO_SMALL` - Value below minimum length/value
101
+ - `ErrorCode.TOO_BIG` - Value above maximum length/value
102
+ - `ErrorCode.INVALID_FORMAT` - Invalid format (email, uuid, url, etc.)
103
+ - `ErrorCode.CUSTOM` - Custom validation errors
104
+
105
+ ### Format Types
106
+
107
+ `FormatType.EMAIL`, `FormatType.UUID`, `FormatType.URL`, `FormatType.REGEX`, `FormatType.CUID`, `FormatType.CUID2`, `FormatType.ULID`, `FormatType.IP`, `FormatType.DATE`, `FormatType.DATETIME`, `FormatType.TIME`
108
+
109
+ ## License
110
+
111
+ MIT
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "zod-error-map",
3
+ "version": "0.1.0",
4
+ "description": "Type-safe, customizable error message mapping for Zod validation",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "types": "src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./src/index.js",
11
+ "types": "./src/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "src/**/*.js",
16
+ "src/**/*.d.ts",
17
+ "!src/**/*.test.js"
18
+ ],
19
+ "scripts": {
20
+ "test": "node --test 'tests/**/*.test.js'",
21
+ "typecheck": "tsc"
22
+ },
23
+ "keywords": [
24
+ "zod",
25
+ "validation",
26
+ "error",
27
+ "error-map",
28
+ "error-messages",
29
+ "typescript"
30
+ ],
31
+ "author": "Manoel Lopes",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/manoel-lopes/zod-error-map.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/manoel-lopes/zod-error-map/issues"
39
+ },
40
+ "homepage": "https://github.com/manoel-lopes/zod-error-map#readme",
41
+ "peerDependencies": {
42
+ "zod": "^3.0.0 || ^4.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "typescript": "^5.0.0",
46
+ "zod": "^4.1.0"
47
+ },
48
+ "engines": {
49
+ "node": ">=18"
50
+ }
51
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Zod error codes
3
+ * @readonly
4
+ * @enum {string}
5
+ */
6
+ export const ErrorCode = /** @type {const} */ ({
7
+ INVALID_TYPE: 'invalid_type',
8
+ TOO_SMALL: 'too_small',
9
+ TOO_BIG: 'too_big',
10
+ INVALID_FORMAT: 'invalid_format',
11
+ CUSTOM: 'custom',
12
+ })
13
+
14
+ /**
15
+ * Format types for invalid_format errors
16
+ * @readonly
17
+ * @enum {string}
18
+ */
19
+ export const FormatType = /** @type {const} */ ({
20
+ EMAIL: 'email',
21
+ UUID: 'uuid',
22
+ URL: 'url',
23
+ REGEX: 'regex',
24
+ CUID: 'cuid',
25
+ CUID2: 'cuid2',
26
+ ULID: 'ulid',
27
+ IP: 'ip',
28
+ EMOJI: 'emoji',
29
+ DATE: 'date',
30
+ DATETIME: 'datetime',
31
+ TIME: 'time',
32
+ })
@@ -0,0 +1,87 @@
1
+ import { createDefaultBuilders } from './message-builders.js'
2
+
3
+ /**
4
+ * @typedef {import('./types.js').RawIssue} RawIssue
5
+ * @typedef {import('./types.js').Label} Label
6
+ * @typedef {import('./types.js').MessageBuilder} MessageBuilder
7
+ * @typedef {import('./types.js').ErrorMapConfig} ErrorMapConfig
8
+ */
9
+
10
+ const DEFAULT_ERROR = 'Invalid input'
11
+
12
+ /**
13
+ * Checks if the issue is a valid RawIssue
14
+ * @param {unknown} issue
15
+ * @returns {issue is RawIssue}
16
+ */
17
+ function isRawIssue(issue) {
18
+ return (
19
+ typeof issue === 'object' &&
20
+ issue !== null &&
21
+ 'code' in issue
22
+ )
23
+ }
24
+
25
+ /**
26
+ * Gets the label from the issue path
27
+ * @param {RawIssue} issue
28
+ * @returns {Label}
29
+ */
30
+ function getLabel(issue) {
31
+ const path = issue.path ?? []
32
+ const field = path[path.length - 1]
33
+ if (typeof field === 'string') {
34
+ return { bare: field, quoted: `'${field}'` }
35
+ }
36
+ return { bare: 'value', quoted: 'value' }
37
+ }
38
+
39
+ /**
40
+ * Creates a Zod error mapper with customizable message builders
41
+ * @param {ErrorMapConfig} [config]
42
+ * @returns {{ format: (issue: unknown) => string, createErrorMap: () => (issue: unknown) => string }}
43
+ */
44
+ export function createErrorMapper(config = {}) {
45
+ const defaultError = config.defaultError ?? DEFAULT_ERROR
46
+ const defaultBuilders = createDefaultBuilders(defaultError, config.formatMessages)
47
+ const builders = { ...defaultBuilders, ...config.builders }
48
+
49
+ /**
50
+ * Builds the error message for a Zod issue
51
+ * @param {RawIssue} issue
52
+ * @returns {string}
53
+ */
54
+ function buildMessage(issue) {
55
+ const label = getLabel(issue)
56
+ const builder = builders[issue.code]
57
+ if (builder) {
58
+ return builder(issue, label)
59
+ }
60
+ return issue.message || defaultError
61
+ }
62
+
63
+ /**
64
+ * Formats a Zod issue into a human-readable message
65
+ * @param {unknown} issue
66
+ * @returns {string}
67
+ */
68
+ function format(issue) {
69
+ if (!isRawIssue(issue)) return defaultError
70
+ return buildMessage(issue)
71
+ }
72
+
73
+ /**
74
+ * Creates an error map function for Zod
75
+ * @returns {(issue: unknown) => string}
76
+ */
77
+ function createErrorMap() {
78
+ return format
79
+ }
80
+
81
+ return { format, createErrorMap }
82
+ }
83
+
84
+ /**
85
+ * Default error mapper instance
86
+ */
87
+ export const defaultErrorMapper = createErrorMapper()
package/src/index.d.ts ADDED
@@ -0,0 +1,77 @@
1
+ import type { z } from 'zod'
2
+
3
+ export interface Label {
4
+ bare: string
5
+ quoted: string
6
+ }
7
+
8
+ export interface RawIssue {
9
+ code: string
10
+ path: Array<string | number>
11
+ input?: unknown
12
+ expected?: string
13
+ minimum?: number
14
+ maximum?: number
15
+ format?: string
16
+ message?: string
17
+ }
18
+
19
+ export type MessageBuilder = (issue: RawIssue, label: Label) => string
20
+
21
+ export type FormatMessageBuilder = (label: Label) => string
22
+
23
+ export interface ErrorMapConfig {
24
+ defaultError?: string
25
+ builders?: Record<string, MessageBuilder>
26
+ formatMessages?: Record<string, FormatMessageBuilder>
27
+ }
28
+
29
+ export interface ErrorMapper {
30
+ format: (issue: unknown) => string
31
+ createErrorMap: () => (issue: unknown) => string
32
+ }
33
+
34
+ export declare const ErrorCode: {
35
+ readonly INVALID_TYPE: 'invalid_type'
36
+ readonly TOO_SMALL: 'too_small'
37
+ readonly TOO_BIG: 'too_big'
38
+ readonly INVALID_FORMAT: 'invalid_format'
39
+ readonly CUSTOM: 'custom'
40
+ }
41
+
42
+ export declare const FormatType: {
43
+ readonly EMAIL: 'email'
44
+ readonly UUID: 'uuid'
45
+ readonly URL: 'url'
46
+ readonly REGEX: 'regex'
47
+ readonly CUID: 'cuid'
48
+ readonly CUID2: 'cuid2'
49
+ readonly ULID: 'ulid'
50
+ readonly IP: 'ip'
51
+ readonly EMOJI: 'emoji'
52
+ readonly DATE: 'date'
53
+ readonly DATETIME: 'datetime'
54
+ readonly TIME: 'time'
55
+ }
56
+
57
+ export declare function createErrorMapper(config?: ErrorMapConfig): ErrorMapper
58
+ export declare const defaultErrorMapper: ErrorMapper
59
+
60
+ export declare function getInputDescription(input: unknown): string
61
+ export declare const defaultFormatMessages: Record<string, (label: Label) => string>
62
+ export declare function buildInvalidTypeMessage(issue: RawIssue, label: Label): string
63
+ export declare function buildTooSmallMessage(issue: RawIssue, label: Label): string
64
+ export declare function buildTooBigMessage(issue: RawIssue, label: Label): string
65
+ export declare function createFormatMessageBuilder(
66
+ formatMessages?: Record<string, (label: Label) => string>
67
+ ): MessageBuilder
68
+ export declare function createCustomMessageBuilder(defaultError: string): MessageBuilder
69
+ export declare function createDefaultBuilders(
70
+ defaultError: string,
71
+ formatMessages?: Record<string, (label: Label) => string>
72
+ ): Record<string, MessageBuilder>
73
+
74
+ export declare function setZodErrorMap(z: typeof import('zod').z, config?: ErrorMapConfig): void
75
+ export declare function createZodErrorMap(
76
+ config?: ErrorMapConfig
77
+ ): (issue: unknown, ctx: unknown) => { message: string }
package/src/index.js ADDED
@@ -0,0 +1,22 @@
1
+ export { ErrorCode, FormatType } from './error-codes.js'
2
+
3
+ export {
4
+ createErrorMapper,
5
+ defaultErrorMapper,
6
+ } from './error-mapper.js'
7
+
8
+ export {
9
+ getInputDescription,
10
+ defaultFormatMessages,
11
+ buildInvalidTypeMessage,
12
+ buildTooSmallMessage,
13
+ buildTooBigMessage,
14
+ createFormatMessageBuilder,
15
+ createCustomMessageBuilder,
16
+ createDefaultBuilders,
17
+ } from './message-builders.js'
18
+
19
+ export {
20
+ setZodErrorMap,
21
+ createZodErrorMap,
22
+ } from './zod-integration.js'
@@ -0,0 +1,108 @@
1
+ import { ErrorCode, FormatType } from './error-codes.js'
2
+
3
+ /**
4
+ * @typedef {import('./types.js').RawIssue} RawIssue
5
+ * @typedef {import('./types.js').Label} Label
6
+ * @typedef {import('./types.js').MessageBuilder} MessageBuilder
7
+ */
8
+
9
+ /**
10
+ * Gets a human-readable description of the input type
11
+ * @param {unknown} input
12
+ * @returns {string}
13
+ */
14
+ export function getInputDescription(input) {
15
+ if (input === undefined) return 'undefined'
16
+ if (input === null) return 'null'
17
+ if (Array.isArray(input)) return 'array'
18
+ return typeof input
19
+ }
20
+
21
+ /**
22
+ * Default format error messages
23
+ * @type {Record<string, (label: Label) => string>}
24
+ */
25
+ export const defaultFormatMessages = {
26
+ [FormatType.EMAIL]: (label) => `The ${label.quoted} must be a valid email address`,
27
+ [FormatType.UUID]: (label) => `The ${label.quoted} must be a valid UUID`,
28
+ [FormatType.URL]: (label) => `The ${label.quoted} must be a valid URL`,
29
+ [FormatType.REGEX]: (label) => `The ${label.quoted} has an invalid format`,
30
+ [FormatType.CUID]: (label) => `The ${label.quoted} must be a valid CUID`,
31
+ [FormatType.CUID2]: (label) => `The ${label.quoted} must be a valid CUID2`,
32
+ [FormatType.ULID]: (label) => `The ${label.quoted} must be a valid ULID`,
33
+ [FormatType.IP]: (label) => `The ${label.quoted} must be a valid IP address`,
34
+ [FormatType.DATE]: (label) => `The ${label.quoted} must be a valid date`,
35
+ [FormatType.DATETIME]: (label) => `The ${label.quoted} must be a valid datetime`,
36
+ [FormatType.TIME]: (label) => `The ${label.quoted} must be a valid time`,
37
+ }
38
+
39
+ /**
40
+ * Builds error message for invalid_type errors
41
+ * @type {MessageBuilder}
42
+ */
43
+ export function buildInvalidTypeMessage(issue, label) {
44
+ const received = getInputDescription(issue.input)
45
+ if (received === 'undefined') {
46
+ return `The ${label.bare} is required`
47
+ }
48
+ return `Expected ${issue.expected} for ${label.quoted}, received ${received}`
49
+ }
50
+
51
+ /**
52
+ * Builds error message for too_small errors
53
+ * @type {MessageBuilder}
54
+ */
55
+ export function buildTooSmallMessage(issue, label) {
56
+ const min = Number(issue.minimum) || 0
57
+ return `The ${label.quoted} must contain at least ${min} characters`
58
+ }
59
+
60
+ /**
61
+ * Builds error message for too_big errors
62
+ * @type {MessageBuilder}
63
+ */
64
+ export function buildTooBigMessage(issue, label) {
65
+ const max = Number(issue.maximum) || 0
66
+ return `The ${label.quoted} must contain at most ${max} characters`
67
+ }
68
+
69
+ /**
70
+ * Creates a format message builder with custom format messages
71
+ * @param {Record<string, (label: Label) => string>} [formatMessages]
72
+ * @returns {MessageBuilder}
73
+ */
74
+ export function createFormatMessageBuilder(formatMessages = defaultFormatMessages) {
75
+ return (issue, label) => {
76
+ const format = issue.format || ''
77
+ const messageBuilder = formatMessages[format]
78
+ if (messageBuilder) {
79
+ return messageBuilder(label)
80
+ }
81
+ return `The ${label.quoted} has an invalid format`
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Builds error message for custom errors
87
+ * @param {string} defaultError
88
+ * @returns {MessageBuilder}
89
+ */
90
+ export function createCustomMessageBuilder(defaultError) {
91
+ return (issue, _label) => issue.message || defaultError
92
+ }
93
+
94
+ /**
95
+ * Creates the default message builders map
96
+ * @param {string} defaultError
97
+ * @param {Record<string, (label: Label) => string>} [formatMessages]
98
+ * @returns {Record<string, MessageBuilder>}
99
+ */
100
+ export function createDefaultBuilders(defaultError, formatMessages) {
101
+ return {
102
+ [ErrorCode.INVALID_TYPE]: buildInvalidTypeMessage,
103
+ [ErrorCode.TOO_SMALL]: buildTooSmallMessage,
104
+ [ErrorCode.TOO_BIG]: buildTooBigMessage,
105
+ [ErrorCode.INVALID_FORMAT]: createFormatMessageBuilder(formatMessages),
106
+ [ErrorCode.CUSTOM]: createCustomMessageBuilder(defaultError),
107
+ }
108
+ }
package/src/types.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @typedef {object} Label
3
+ * @property {string} bare - The field name without quotes
4
+ * @property {string} quoted - The field name with quotes
5
+ */
6
+
7
+ /**
8
+ * @typedef {object} RawIssue
9
+ * @property {string} code - The error code from Zod
10
+ * @property {Array<string | number>} path - The path to the field
11
+ * @property {unknown} [input] - The input value that caused the error
12
+ * @property {string} [expected] - The expected type
13
+ * @property {number} [minimum] - Minimum value/length constraint
14
+ * @property {number} [maximum] - Maximum value/length constraint
15
+ * @property {string} [format] - Format type (email, uuid, url, etc.)
16
+ * @property {string} [message] - Custom error message
17
+ */
18
+
19
+ /**
20
+ * @callback MessageBuilder
21
+ * @param {RawIssue} issue - The raw Zod issue
22
+ * @param {Label} label - The field label
23
+ * @returns {string} The formatted error message
24
+ */
25
+
26
+ /**
27
+ * @typedef {(label: Label) => string} FormatMessageBuilder
28
+ */
29
+
30
+ /**
31
+ * @typedef {object} ErrorMapConfig
32
+ * @property {string} [defaultError] - Default error message
33
+ * @property {Record<string, MessageBuilder>} [builders] - Custom message builders by error code
34
+ * @property {Record<string, FormatMessageBuilder>} [formatMessages] - Custom messages for format errors
35
+ */
36
+
37
+ export {}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @typedef {import('./types.js').ErrorMapConfig} ErrorMapConfig
3
+ */
4
+
5
+ import { createErrorMapper } from './error-mapper.js'
6
+
7
+ /**
8
+ * Sets the global Zod error map using z.config()
9
+ * @param {import('zod')} z - The Zod instance
10
+ * @param {ErrorMapConfig} [config] - Optional configuration
11
+ * @returns {void}
12
+ */
13
+ export function setZodErrorMap(z, config) {
14
+ const mapper = createErrorMapper(config)
15
+ z.config({
16
+ customError: (issue) => mapper.format(issue),
17
+ })
18
+ }
19
+
20
+ /**
21
+ * Creates a Zod error map function compatible with z.setErrorMap() (Zod v3)
22
+ * @param {ErrorMapConfig} [config] - Optional configuration
23
+ * @returns {(issue: unknown, ctx: unknown) => { message: string }}
24
+ */
25
+ export function createZodErrorMap(config) {
26
+ const mapper = createErrorMapper(config)
27
+ return (issue, _ctx) => {
28
+ const message = mapper.format(issue)
29
+ return { message }
30
+ }
31
+ }