Skip to content

Conversation

@marjan-ahmed
Copy link
Contributor

Overview

Adds support for custom, typed error codes to enable proper internationalization (i18n) of validation errors. This allows developers to specify semantic error identifiers that can be mapped to localized messages at runtime.

Problem

  • Built-in validators have error codes (too_small, too_big, etc.) but they're untyped
  • .refine() custom validations only get a generic "custom" code
  • No way to define semantic error codes for business logic validation
  • Makes i18n difficult - must parse error messages or use untyped string codes

Solution

Extends Zod's error system to support optional code parameter in validators and refinements, enabling type-safe, semantic error identifiers.

Changes

Core Type Updates

  • Added customCode?: string to ZodIssueBase (inherited by all issue types)
  • Extended errorUtil.ErrMessage to accept { message?: string; code?: string }
  • Updated ZodStringCheck and ZodNumberCheck to support optional code field

Validator Updates

String validators:

  • .min(), .max(), .length(), .email(), .url(), .uuid(), etc.

Number validators:

  • .min(), .max(), .int(), .positive(), .negative(), etc.

Custom validators:

  • .refine() and .superRefine() now accept customCode parameter

API Usage

Built-in validators:

const schema = z.string().min(5, {
  message: "Username too short",
  code: "USERNAME_TOO_SHORT"
});

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 29, 2025

Walkthrough

This pull request introduces optional custom error codes throughout the Zod validation library. It adds a customCode field to ZodIssue types, extends error utilities to carry code metadata, and updates ZodStringCheck and ZodNumberCheck to accept optional codes that propagate to generated errors. A comprehensive test suite validates the feature across validators, refinements, async validation, and real-world scenarios. Example code demonstrates integration patterns for i18n-based error translations. No breaking changes to existing APIs.

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "feat: Add typed error codes for i18n support" directly aligns with the main objective of the changeset. The pull request fundamentally extends Zod's error system by adding customCode fields to ZodIssueBase and related types, updating ErrMessage and check types to support optional code parameters, and enabling developers to attach semantic error codes for internationalization. The title is concise, specific, and clearly summarizes the primary feature being introduced without unnecessary noise or vagueness.
Description Check ✅ Passed The PR description is directly related to the changeset and provides substantial context. It explains the problem (untyped error codes and lack of semantic identifiers), the solution (extending the error system with optional code parameters), and details specific changes like adding customCode to ZodIssueBase, extending ErrMessage type, and updating string/number validators. The description includes concrete API usage examples and demonstrates clear understanding of how the feature addresses i18n needs, making it highly relevant to the actual code modifications across multiple files.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d336c4 and f95d7f6.

📒 Files selected for processing (5)
  • packages/zod/src/v3/ZodError.ts (4 hunks)
  • packages/zod/src/v3/helpers/errorUtil.ts (1 hunks)
  • packages/zod/src/v3/tests/typed-error-codes.test.ts (1 hunks)
  • packages/zod/src/v3/types.ts (36 hunks)
  • play.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (13)
**/*.{js,jsx,ts,tsx,mjs,cjs,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Enforce line width of 120 characters via Biome formatting

Files:

  • packages/zod/src/v3/ZodError.ts
  • packages/zod/src/v3/tests/typed-error-codes.test.ts
  • play.ts
  • packages/zod/src/v3/helpers/errorUtil.ts
  • packages/zod/src/v3/types.ts
**/*.{js,jsx,ts,tsx,mjs,cjs}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ES5-style trailing commas in JavaScript/TypeScript code

Files:

  • packages/zod/src/v3/ZodError.ts
  • packages/zod/src/v3/tests/typed-error-codes.test.ts
  • play.ts
  • packages/zod/src/v3/helpers/errorUtil.ts
  • packages/zod/src/v3/types.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Allow the any type in TypeScript (noExplicitAny off)
Allow non-null assertions in TypeScript (noNonNullAssertion off)
Write TypeScript to pass strict mode with exactOptionalPropertyTypes enabled
Use NodeNext module resolution semantics for imports in TypeScript
Target ES2020 language features in TypeScript source

Files:

  • packages/zod/src/v3/ZodError.ts
  • packages/zod/src/v3/tests/typed-error-codes.test.ts
  • play.ts
  • packages/zod/src/v3/helpers/errorUtil.ts
  • packages/zod/src/v3/types.ts
**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (CLAUDE.md)

Allow parameter reassignment for performance-sensitive code (noParameterAssign off)

Files:

  • packages/zod/src/v3/ZodError.ts
  • packages/zod/src/v3/tests/typed-error-codes.test.ts
  • play.ts
  • packages/zod/src/v3/helpers/errorUtil.ts
  • packages/zod/src/v3/types.ts
**/*.{js,mjs,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development-setup.mdc)

**/*.{js,mjs,ts,tsx}: Use .js extensions in import specifiers (e.g., import { z } from "./index.js")
Don’t use require(); use ESM import statements

Files:

  • packages/zod/src/v3/ZodError.ts
  • packages/zod/src/v3/tests/typed-error-codes.test.ts
  • play.ts
  • packages/zod/src/v3/helpers/errorUtil.ts
  • packages/zod/src/v3/types.ts
**/*.{js,mjs,cjs,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/guidelines.mdc)

Do not leave log statements (e.g., console.log, debugger) in tests or production code

Files:

  • packages/zod/src/v3/ZodError.ts
  • packages/zod/src/v3/tests/typed-error-codes.test.ts
  • play.ts
  • packages/zod/src/v3/helpers/errorUtil.ts
  • packages/zod/src/v3/types.ts
packages/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/zod-project-guide.mdc)

Write source code in TypeScript (TypeScript-first codebase)

Files:

  • packages/zod/src/v3/ZodError.ts
  • packages/zod/src/v3/tests/typed-error-codes.test.ts
  • packages/zod/src/v3/helpers/errorUtil.ts
  • packages/zod/src/v3/types.ts
packages/zod/**

📄 CodeRabbit inference engine (.cursor/rules/zod-project-guide.mdc)

Make core Zod library changes in the main package at packages/zod/

Files:

  • packages/zod/src/v3/ZodError.ts
  • packages/zod/src/v3/tests/typed-error-codes.test.ts
  • packages/zod/src/v3/helpers/errorUtil.ts
  • packages/zod/src/v3/types.ts
**/src/*/tests/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place all test files under src/*/tests/ directories

Files:

  • packages/zod/src/v3/tests/typed-error-codes.test.ts
packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*

📄 CodeRabbit inference engine (.cursor/rules/testing-guidelines.mdc)

Place all test files under packages/zod/src/v4/classic/tests, packages/zod/src/v4/core/tests, or packages/zod/src/v3/tests

Files:

  • packages/zod/src/v3/tests/typed-error-codes.test.ts
packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts

📄 CodeRabbit inference engine (.cursor/rules/testing-guidelines.mdc)

packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts: Test files must use the .test.ts extension (TypeScript), not JavaScript
Use import type for type-only imports in tests (e.g., import type { ... })
Permanent, regression, API validation, edge-case coverage, and performance benchmark tests must be in the test suite (not play.ts)
Use Vitest as the framework in tests and import from it as import { expect, test } from "vitest"
Import Zod in tests as import * as z from "zod/v4"
Write tests with clear, descriptive names and cover both success and failure cases
Keep test suites concise while maintaining adequate coverage
Do not skip tests due to type issues; fix the types instead
Use descriptive file names like string.test.ts, object.test.ts, url-validation.test.ts and group related functionality together

Files:

  • packages/zod/src/v3/tests/typed-error-codes.test.ts
packages/**/*.test.ts

📄 CodeRabbit inference engine (.cursor/rules/zod-project-guide.mdc)

Use Vitest for tests and place test cases in .test.ts files

Files:

  • packages/zod/src/v3/tests/typed-error-codes.test.ts
play.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use play.ts as the entry for quick experiments with pnpm dev play.ts

Use play.ts for quick experimentation, manual debugging, and temporary validation before writing formal tests

Files:

  • play.ts
🧠 Learnings (19)
📓 Common learnings
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/errors.ts : Define and use canonical error types/codes as declared in errors.ts (e.g., invalid_type, TooBig/TooSmall, InvalidStringFormat, InvalidUnion, Custom)
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/checks.ts : Implement checks via inst._zod.check(payload) that only pushes issues when validation fails and sets continue based on def.abort
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/{schemas.ts,checks.ts} : When adding issues, push well-formed payload.issues entries including code, expected (when applicable), input, inst, and optional path/message/continue
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/errors.ts : Define and use canonical error types/codes as declared in errors.ts (e.g., invalid_type, TooBig/TooSmall, InvalidStringFormat, InvalidUnion, Custom)

Applied to files:

  • packages/zod/src/v3/ZodError.ts
  • packages/zod/src/v3/tests/typed-error-codes.test.ts
  • play.ts
  • packages/zod/src/v3/helpers/errorUtil.ts
  • packages/zod/src/v3/types.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/{schemas.ts,checks.ts} : When adding issues, push well-formed payload.issues entries including code, expected (when applicable), input, inst, and optional path/message/continue

Applied to files:

  • packages/zod/src/v3/ZodError.ts
  • packages/zod/src/v3/types.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/util.ts : Use util.prefixIssues() and util.finalizeIssue() to construct and modify issue objects consistently

Applied to files:

  • packages/zod/src/v3/ZodError.ts
  • packages/zod/src/v3/helpers/errorUtil.ts
  • packages/zod/src/v3/types.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/checks.ts : Implement checks via inst._zod.check(payload) that only pushes issues when validation fails and sets continue based on def.abort

Applied to files:

  • packages/zod/src/v3/ZodError.ts
  • packages/zod/src/v3/types.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Do not skip tests due to type issues; fix the types instead

Applied to files:

  • packages/zod/src/v3/ZodError.ts
  • packages/zod/src/v3/tests/typed-error-codes.test.ts
  • packages/zod/src/v3/types.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Import Zod in tests as `import * as z from "zod/v4"`

Applied to files:

  • packages/zod/src/v3/ZodError.ts
  • packages/zod/src/v3/tests/typed-error-codes.test.ts
  • packages/zod/src/v3/types.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/{schemas.ts,core.ts} : Use the custom constructor system via core.$constructor() and initialize instances with $ZodType.init() when creating schemas

Applied to files:

  • packages/zod/src/v3/ZodError.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Write tests with clear, descriptive names and cover both success and failure cases

Applied to files:

  • packages/zod/src/v3/tests/typed-error-codes.test.ts
  • packages/zod/src/v3/types.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Use descriptive file names like string.test.ts, object.test.ts, url-validation.test.ts and group related functionality together

Applied to files:

  • packages/zod/src/v3/tests/typed-error-codes.test.ts
  • play.ts
  • packages/zod/src/v3/types.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Permanent, regression, API validation, edge-case coverage, and performance benchmark tests must be in the test suite (not play.ts)

Applied to files:

  • packages/zod/src/v3/tests/typed-error-codes.test.ts
  • play.ts
  • packages/zod/src/v3/types.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Use import type for type-only imports in tests (e.g., `import type { ... }`)

Applied to files:

  • packages/zod/src/v3/tests/typed-error-codes.test.ts
  • packages/zod/src/v3/types.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Keep test suites concise while maintaining adequate coverage

Applied to files:

  • packages/zod/src/v3/tests/typed-error-codes.test.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Use Vitest as the framework in tests and import from it as `import { expect, test } from "vitest"`

Applied to files:

  • packages/zod/src/v3/tests/typed-error-codes.test.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Test files must use the .test.ts extension (TypeScript), not JavaScript

Applied to files:

  • packages/zod/src/v3/tests/typed-error-codes.test.ts
📚 Learning: 2025-10-21T17:26:32.924Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/testing-workflow.mdc:0-0
Timestamp: 2025-10-21T17:26:32.924Z
Learning: Applies to packages/zod/src/v4/{classic,core}/tests/**/*.test.ts : Use Vitest for all testing (Vitest APIs in test files)

Applied to files:

  • packages/zod/src/v3/tests/typed-error-codes.test.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Follow the workflow: start with play.ts for exploration, then write proper tests, cover edge cases and errors, verify backward compatibility, and run the full suite before committing

Applied to files:

  • play.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Adhere to best practices: verify wrapper passthrough, prefer util.defineLazy for computed props, follow existing schema patterns, use precise TypeScript types, and document complex internals

Applied to files:

  • play.ts
📚 Learning: 2025-10-21T17:24:39.708Z
Learnt from: CR
PR: colinhacks/zod#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-21T17:24:39.708Z
Learning: All changes must pass tests and type checking

Applied to files:

  • play.ts
🧬 Code graph analysis (3)
packages/zod/src/v3/tests/typed-error-codes.test.ts (2)
packages/zod/src/v3/types.ts (3)
  • schema (4029-4031)
  • data (247-290)
  • email (1093-1095)
packages/zod/src/v4/core/util.ts (1)
  • issue (838-850)
play.ts (2)
packages/zod/src/v3/types.ts (2)
  • data (247-290)
  • error (100-105)
packages/zod/src/v4/core/util.ts (1)
  • issue (838-850)
packages/zod/src/v3/types.ts (2)
packages/zod/src/v3/ZodError.ts (3)
  • message (284-286)
  • ZodIssueCode (15-32)
  • ZodIssueCode (34-34)
packages/zod/src/v3/helpers/errorUtil.ts (1)
  • ErrMessage (2-2)
🔇 Additional comments (5)
packages/zod/src/v3/ZodError.ts (1)

36-40: Custom code hook looks nice.

Sliding the optional customCode into the base issue shape keeps the rest of the system simple. Clean work.

packages/zod/src/v3/helpers/errorUtil.ts (1)

2-7: errToObj upgrade feels smooth.

Letting errToObj surface { message, code } keeps all the call sites happy without extra juggling. Dig it.

packages/zod/src/v3/tests/typed-error-codes.test.ts (1)

4-436: Loving the coverage.

These tests hammer every path I cared about—built-ins, refinements, async, and the i18n mapping. Super reassuring.

packages/zod/src/v3/types.ts (2)

760-1035: Great wiring on the string checks.

Every failure branch now drops the customCode in alongside the canonical code—exactly what callers need. Nicely done.


1424-1630: Same story on the number side.

Really appreciate how the numeric checks share the same pattern—hard to misuse and super consistent.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant