feat: MAC address validation in v4 and mini (#5440)

* feat: add z.mac() and ZodMAC

* feat: reject nonstandard and 64-bit mac addrs

* test: MAC addr validation

* docs: add z.mac()

* feat(mini): add z.mac()

* fix: tighten z.mac() test cases

* Add delimiter, drop version

---------

Co-authored-by: Colin McDonnell <[email protected]>
This commit is contained in:
Nathaniel Gerlek
2025-11-17 21:48:14 -08:00
committed by GitHub
co-authored by Colin McDonnell
parent eae131c258
commit 775f3fe45f
23 changed files with 303 additions and 738 deletions
-78
View File
@@ -1,78 +0,0 @@
# Development Setup
## Prerequisites
### Node.js
- **Required**: Node.js v24+ (project uses modern ES modules and features)
- **Recommended**: Use nvm for Node.js version management
- **Setup**: `nvm use default` to use the latest Node.js version
### Package Manager
- **Required**: pnpm v10.12.1 (specified in package.json)
- **Install**: `npm install -g [email protected]`
## Environment Setup
### 1. Install Dependencies
```bash
pnpm install
```
### 2. Verify Setup
```bash
node --version # Should be v24+
pnpm --version # Should be 10.12.1
```
### 3. Build Project
```bash
pnpm build
```
### 4. Run Tests
```bash
pnpm test
```
## Development Workflow
### Starting Development
```bash
pnpm dev # Start development server
pnpm dev:watch # Start with watch mode
pnpm dev:play # Run play.ts for testing
```
### Code Quality
```bash
pnpm format # Format code
pnpm lint # Lint and fix
pnpm fix # Format + lint
```
### Testing
```bash
pnpm test # Run all tests
pnpm test:watch # Run tests in watch mode
```
## Troubleshooting
### Node.js Version Issues
- Ensure you're using Node.js v24+
- Use `nvm use default` to switch to correct version
- Check with `node --version`
### Module Import Issues
- Project uses ES modules (`"type": "module"`)
- Use `.js` extensions in imports: `import { z } from "./index.js"`
- Don't use `require()` - use `import` statements
### Test Issues
- Ensure Vitest is properly configured
- Check that test files are in correct directories
- Use `pnpm vitest run <specific-file>` for isolated testing
description:
globs:
alwaysApply: false
---
-12
View File
@@ -1,12 +0,0 @@
# Project Guidelines
This file contains general coding guidelines for the project. Update this file with small rules and best practices that do not merit their own dedicated rule file.
## Guidelines
- Do not leave log statements (e.g., `console.log`, `debugger`) in tests or production code.
description:
globs:
alwaysApply: false
---
- Ask before generating any new files.
-111
View File
@@ -1,111 +0,0 @@
---
description: Testing guidelines for the Zod codebase - when to use play.ts vs test suite, TypeScript requirements, and testing patterns
globs: *.test.ts,*.test.tsx,play.ts
---
# Zod Testing Guidelines
## Always Use TypeScript
- **All tests must be written in TypeScript** - never use JavaScript
- Test files should use `.test.ts` extension
- Import types properly: `import type { ... }`
## When to Use play.ts vs Test Suite
### Use [play.ts](mdc:play.ts) for:
- Quick experimentation and prototyping
- Manual testing of new features during development
- Debugging specific behaviors
- One-off validation of edge cases
- Temporary verification before writing formal tests
### Use Test Suite for:
- **All permanent test cases** - features must have proper tests
- Regression testing
- API validation
- Edge case coverage
- Performance benchmarks
## Test Organization
### Test File Structure
Tests are located in:
- `packages/zod/src/v4/classic/tests/` - V4 classic API tests
- `packages/zod/src/v4/core/tests/` - V4 core tests
- `packages/zod/src/v3/tests/` - V3 compatibility tests
### Test File Naming
- Use descriptive names: `string.test.ts`, `object.test.ts`, `url-validation.test.ts`
- Group related functionality in the same test file
- Follow existing patterns in [packages/zod/src/v4/classic/tests/string.test.ts](mdc:packages/zod/src/v4/classic/tests/string.test.ts)
## Test Writing Patterns
### Framework Usage
- Use **Vitest** as the test framework
- Import: `import { expect, test } from "vitest";`
- Import Zod: `import * as z from "zod/v4";`
### Test Structure
```typescript
test("descriptive test name", () => {
const schema = z.string().url();
// Test valid cases
expect(schema.parse("https://example.com")).toBe("https://example.com");
// Test invalid cases
expect(() => schema.parse("invalid")).toThrow();
});
```
### Testing Preferences
- **Keep test suites concise** - use as few test cases as needed to cover functionality
- **Don't skip tests** due to type issues - fix the types instead
- **Don't oversimplify tests** - maintain adequate coverage
- Test both success and failure cases
- Use descriptive test names that explain the behavior being tested
### URL Testing Example
When testing URL validation (as seen in recent work):
```typescript
test("url preserves original input", () => {
const url = z.string().url();
const input = "https://example.com?key=value";
expect(url.parse(input)).toBe(input);
});
test("url normalize flag", () => {
const normalizeUrl = z.url({ normalize: true });
expect(normalizeUrl.parse("https://example.com?key=value"))
.toBe("https://example.com/?key=value");
});
```
## Running Tests
### Run Specific Tests
```bash
pnpm vitest run src/v4/classic/tests/string.test.ts -t "url"
```
### Run All Tests
```bash
pnpm test
```
### Build Before Testing
Always rebuild when testing core changes:
```bash
pnpm build && pnpm test
```
## Test Development Workflow
1. **Start with [play.ts](mdc:play.ts)** for initial exploration
2. **Write proper tests** once behavior is confirmed
3. **Test edge cases** and error conditions
4. **Verify backward compatibility**
5. **Run full test suite** before committing
Remember: Features without tests are incomplete. Every new feature or bug fix should include corresponding test coverage.
-56
View File
@@ -1,56 +0,0 @@
# Testing Workflow
## Test Framework
- **Vitest** is used for all testing
- Tests are located in `packages/zod/src/v4/classic/tests/` and `packages/zod/src/v4/core/tests/`
- Test files follow the pattern `*.test.ts`
## Running Tests
### All Tests
```bash
pnpm test
```
### Watch Mode
```bash
pnpm test:watch
```
### Specific Test File
```bash
pnpm vitest run packages/zod/src/v4/classic/tests/discriminated-unions.test.ts
```
### Specific Test Pattern
```bash
pnpm vitest run --reporter=verbose packages/zod/src/v4/classic/tests/
```
## Test Conventions
- Use `test()` for individual test cases
- Use `describe()` for test groups
- Use `expect()` for assertions
- Test files should be co-located with source files or in dedicated test directories
- Follow the existing test patterns in the codebase
## Test Structure Example
```typescript
import { z } from "../../index.js";
test("test description", () => {
const schema = z.string();
const result = schema.parse("test");
expect(result).toBe("test");
});
```
## Debugging Tests
- Use `console.log()` for debugging
- Run specific test files to isolate issues
- Use watch mode for rapid iteration
description:
globs:
alwaysApply: false
---
-280
View File
@@ -1,280 +0,0 @@
# Zod Internals Comprehensive Guide
## Core Architecture
### Schema Type System
All schemas extend `$ZodType` and implement `$ZodTypeInternals`. The core architecture is defined in:
- [packages/zod/src/v4/core/schemas.ts](mdc:packages/zod/src/v4/core/schemas.ts) - All schema type definitions
- [packages/zod/src/v4/core/core.ts](mdc:packages/zod/src/v4/core/core.ts) - Constructor system and type helpers
- [packages/zod/src/v4/core/util.ts](mdc:packages/zod/src/v4/core/util.ts) - Utility functions and type helpers
### Constructor Pattern
Zod uses a custom constructor system via `$constructor()` function:
```typescript
export const $ZodReadonly: core.$constructor<$ZodReadonly> = /*@__PURE__*/ core.$constructor(
"$ZodReadonly",
(inst, def) => {
$ZodType.init(inst, def);
// Schema-specific initialization
}
);
```
## Key Internal Properties
### Core Properties
- `def`: Schema definition object containing type and configuration
- `traits`: Set of trait identifiers for this schema
- `id`: Unique identifier for the schema instance
- `version`: Zod core version
- `constr`: Constructor function reference
### Validation Properties
- `propValues`: Used for discriminated unions to store literal values per property
- `values`: Set of literal values that pass validation (for enums, literals, etc.)
- `pattern`: RegExp for string validation patterns
- `optin/optout`: Optionality flags for input/output types
- `isst`: Set of possible issue types this schema can throw
### Processing Properties
- `parse`: Function that parses input without running checks
- `run`: Function that parses input and runs all checks
- `deferred`: Array of deferred initializers
- `bag`: Metadata object for schema-specific data
## Schema Categories
### Primitive Schemas
- `$ZodString`, `$ZodNumber`, `$ZodBoolean`, `$ZodBigInt`
- `$ZodSymbol`, `$ZodUndefined`, `$ZodNull`, `$ZodAny`, `$ZodUnknown`
- `$ZodNever`, `$ZodVoid`, `$ZodDate`, `$ZodLiteral`
### Container Schemas
- `$ZodArray`, `$ZodObject`, `$ZodTuple`, `$ZodRecord`
- `$ZodMap`, `$ZodSet`, `$ZodUnion`, `$ZodIntersection`
- `$ZodDiscriminatedUnion`
### Wrapper Schemas
- `$ZodOptional`, `$ZodNullable`, `$ZodReadonly`
- `$ZodDefault`, `$ZodPrefault`, `$ZodNonOptional`
- `$ZodTransform`, `$ZodPipe`, `$ZodCatch`
### Special Schemas
- `$ZodEnum`, `$ZodPromise`, `$ZodLazy`, `$ZodCustom`
- `$ZodTemplateLiteral`, `$ZodNaN`, `$ZodSuccess`
## Common Patterns
### Wrapper Type Pattern
Wrapper types must pass through internal properties from their inner type:
```typescript
// Correct pattern for wrapper types
util.defineLazy(inst._zod, "propValues", () => def.innerType._zod.propValues);
util.defineLazy(inst._zod, "values", () => def.innerType._zod.values);
util.defineLazy(inst._zod, "optin", () => def.innerType._zod.optin);
util.defineLazy(inst._zod, "optout", () => def.innerType._zod.optout);
```
### Lazy Property Definition
Use `util.defineLazy()` to avoid circular dependencies:
```typescript
util.defineLazy(inst._zod, "propValues", () => {
// Computed property logic
return computedValue;
});
```
### Parse Function Pattern
Standard parse function structure:
```typescript
inst._zod.parse = (payload, ctx) => {
// Type checking
if (typeof payload.value !== "expected_type") {
payload.issues.push({
expected: "expected_type",
code: "invalid_type",
input: payload.value,
inst,
});
return payload;
}
// Optional transformation
if (def.coerce) {
payload.value = transform(payload.value);
}
return payload;
};
```
## Error System
### Error Types
Defined in [packages/zod/src/v4/core/errors.ts](mdc:packages/zod/src/v4/core/errors.ts):
- `$ZodIssueInvalidType` - Wrong type provided
- `$ZodIssueTooBig/TooSmall` - Size/length constraints
- `$ZodIssueInvalidStringFormat` - String format validation
- `$ZodIssueInvalidUnion` - Union validation failures
- `$ZodIssueCustom` - Custom validation errors
### Error Creation
```typescript
payload.issues.push({
code: "invalid_type",
expected: "string",
input: payload.value,
inst,
// Optional fields
path: ["property"],
message: "Custom message",
continue: false, // Stop validation
});
```
## Check System
### Check Types
Defined in [packages/zod/src/v4/core/checks.ts](mdc:packages/zod/src/v4/core/checks.ts):
- Numeric checks: `$ZodCheckLessThan`, `$ZodCheckGreaterThan`, `$ZodCheckMultipleOf`
- Size checks: `$ZodCheckMaxSize`, `$ZodCheckMinSize`, `$ZodCheckSizeEquals`
- String checks: `$ZodCheckStringFormat`, `$ZodCheckRegex`, `$ZodCheckIncludes`
- Custom checks: `$ZodCheckProperty`, `$ZodCheckOverwrite`
### Check Pattern
```typescript
inst._zod.check = (payload) => {
if (validationPasses(payload.value)) {
return; // Success
}
payload.issues.push({
code: "validation_code",
// ... other fields
inst,
continue: !def.abort,
});
};
```
## Parsing System
### Parse Functions
Defined in [packages/zod/src/v4/core/parse.ts](mdc:packages/zod/src/v4/core/parse.ts):
- `parse()` - Synchronous parsing
- `parseAsync()` - Asynchronous parsing
- `safeParse()` - Safe parsing (returns result object)
- `safeParseAsync()` - Safe async parsing
### Parse Context
```typescript
interface ParseContextInternal {
async?: boolean;
error?: $ZodErrorMap;
reportInput?: boolean;
jitless?: boolean;
}
```
### Parse Payload
```typescript
interface ParsePayload<T = unknown> {
value: T;
issues: $ZodRawIssue[];
}
```
## Utility Functions
### Type Helpers
- `util.defineLazy()` - Define lazy properties
- `util.cached()` - Create cached getters
- `util.prefixIssues()` - Add path prefixes to issues
- `util.finalizeIssue()` - Finalize raw issues
- `util.clone()` - Clone schemas
### Type Utilities
- `util.PrimitiveSet` - Set of primitive values
- `util.PropValues` - Record of property value sets
- `util.MaybeAsync<T>` - T | Promise<T>
- `util.Literal` - Union of literal types
## Discriminated Unions
### Implementation
Discriminated unions use `propValues` for fast-path validation:
```typescript
// In $ZodDiscriminatedUnion
util.defineLazy(inst._zod, "propValues", () => {
const propValues: util.PropValues = {};
for (const option of def.options) {
const pv = option._zod.propValues;
// Merge propValues from all options
}
return propValues;
});
```
### Requirements
- Each option must have `propValues` with the discriminator key
- Values must be unique across all options
- Used for fast-path validation in `parse()`
## Performance Optimizations
### JIT Compilation
- Fast-path generation for object parsing
- Uses `eval()` when `jitless` is false
- Generates optimized parsing functions
### Lazy Evaluation
- Properties computed on-demand using `util.defineLazy()`
- Avoids circular dependency issues
- Improves initialization performance
### Caching
- `util.cached()` for expensive computations
- Schema normalization caching
- Pattern compilation caching
## Recent Fixes
### Readonly Literal Discriminators
- **Issue**: `$ZodReadonly` wasn't passing through `values` property
- **Fix**: Added `values` to `$ZodReadonlyInternals` and lazy definition
- **Impact**: Readonly literals can now be used as discriminators in discriminated unions
- **Pattern**: Follow the wrapper type pattern for all internal properties
## Development Guidelines
### Creating New Schemas
1. Define the schema definition interface
2. Define the internals interface extending `$ZodTypeInternals`
3. Define the schema interface extending `$ZodType`
4. Create constructor using `$constructor()`
5. Initialize with `$ZodType.init()`
6. Implement `parse()` function
7. Add lazy properties as needed
8. Add tests for both public API and internal properties
### Testing Internals
- Test both public API and internal property access
- Verify `propValues` are correctly computed
- Check that wrapper types pass through properties correctly
- Test error scenarios and edge cases
### Common Pitfalls
- Forgetting to pass through internal properties in wrapper types
- Not using `util.defineLazy()` for computed properties
- Missing error handling in parse functions
- Incorrect issue code usage
- Not testing internal property access
### Best Practices
- Always check if wrapper types pass through required internal properties
- Use `util.defineLazy()` for computed properties to avoid circular dependencies
- Follow existing patterns in similar schema types
- Test both the public API and internal property access
- Use proper TypeScript types for all interfaces
- Document complex internal logic with comments
-62
View File
@@ -1,62 +0,0 @@
# Zod Project Guide
This is the Zod TypeScript-first schema validation library project. The project uses **pnpm** as the package manager and **ES modules**.
## Project Structure
- **Root**: Monorepo with multiple packages
- **Main package**: [packages/zod/](mdc:packages/zod/) - The core Zod library
- **Package manager**: pnpm (version 10.12.1)
- **Module system**: ES modules (`"type": "module"`)
## Common Commands
### Development
- `pnpm dev` - Start development server with tsx
- `pnpm dev:watch` - Start development server with watch mode
- `pnpm dev:play` - Run play.ts file for quick experimentation.
### Testing
- `pnpm test` - Run all tests with vitest
- `pnpm test:watch` - Run tests in watch mode
- `pnpm vitest run <file>` - Run specific test file
- `pnpm vitest run packages/zod/src/v4/classic/tests/discriminated-unions.test.ts` - Run specific test
### Building
- `pnpm build` - Build all packages (excluding docs)
- `pnpm clean` - Clean all packages
### Code Quality
- `pnpm format` - Format code with Biome
- `pnpm format:check` - Check formatting without writing
- `pnpm lint` - Lint and fix code with Biome
- `pnpm lint:check` - Check linting without fixing
- `pnpm fix` - Run both format and lint
### Benchmarking
- `pnpm bench` - Run benchmarks
- `pnpm moltar` - Run specific benchmark (object-moltar)
### Publishing
- `pnpm prepublishOnly` - Run tests and build before publishing
- `pnpm publish:jsr` - Publish to JSR (dry run)
## Key Files
- [package.json](mdc:package.json) - Root package configuration
- [packages/zod/package.json](mdc:packages/zod/package.json) - Main Zod package
- [pnpm-workspace.yaml](mdc:pnpm-workspace.yaml) - Workspace configuration
- [vitest.workspace.ts](mdc:vitest.workspace.ts) - Vitest workspace config
## Development Notes
- Use **pnpm** for all package management
- Tests use **Vitest** framework
- Code formatting/linting uses **Biome**
- The project supports multiple Zod versions (v3, v4, v4-mini)
- Source code uses TypeScript with ES modules
- Node.js v24+ is required for development
description:
globs:
alwaysApply: false
---
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+36
View File
@@ -0,0 +1,36 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
The project uses pnpm workspaces. Key commands:
- `pnpm build` - Build all packages (runs recursive build command)
- `pnpm vitest run` - Run all tests with Vitest
- `pnpm vitest run <path>` - Run specific test file (e.g., `packages/zod/src/v4/classic/tests/string.test.ts`)
- `pnpm vitest run <path> -t "<pattern>"` - Run specific test(s) within a file (e.g., `-t "MAC"`)
- `pnpm vitest run --update` - Update all test snapshots
- `pnpm vitest run <path> --update` - Update snapshots for specific test file
- `pnpm test:watch` - Run tests in watch mode
- `pnpm vitest run --coverage` - Run tests with coverage report
- `pnpm dev` - Execute code with tsx under source conditions
- `pnpm dev <file>` - Execute `<file>` with tsx & proper resolution conditions. Usually use for `play.ts`.
- `pnpm dev:play` - Quick alias to run play.ts for experimentation
- `pnpm lint` - Run biome linter with auto-fix
- `pnpm format` - Format code with biome
- `pnpm fix` - Run both format and lint
## Rules
- Node.js v24+ required (use nvm if needed); pnpm v10.12.1
- ES modules are used throughout (`"type": "module"`)
- All tests must be written in TypeScript - never use JavaScript
- Use `play.ts` for quick experimentation; use proper tests for all permanent test cases
- Features without tests are incomplete - every new feature or bug fix needs test coverage
- Don't skip tests due to type issues - fix the types instead
- Test both success and failure cases with edge cases
- No log statements (`console.log`, `debugger`) in tests or production code
- Ask before generating new files
- Use `util.defineLazy()` for computed properties to avoid circular dependencies
- Performance is critical - parameter reassignment is allowed for optimization
-109
View File
@@ -1,109 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
The project uses pnpm workspaces. Key commands:
- `pnpm build` - Build all packages (runs recursive build command)
- `pnpm test` - Run all tests with Vitest
- `pnpm test:watch` - Run tests in watch mode
- `pnpm dev` - Execute code with tsx under source conditions
- `pnpm dev <file>` - Execute `<file>` with tsx & proper resolution conditions. Usually use for `play.ts`.
- `pnpm lint` - Run biome linter with auto-fix
- `pnpm format` - Format code with biome
- `pnpm fix` - Run both format and lint
### Testing
- Tests use Vitest with workspace-based configuration
- Test files are located in `src/*/tests/` directories
- Run specific tests: `pnpm test <pattern>` or `pnpm test --filter <workspace> <pattern>`
- Tests include type checking via `typecheck.enabled = true`
### Package-Specific Commands
In the `packages/zod/` workspace:
- `pnpm build` - Uses zshy build tool with tsconfig.build.json
- `pnpm clean` - Clean build artifacts (preserving node_modules)
## Architecture Overview
Zod is a TypeScript-first schema validation library organized as a monorepo with multiple versions and variants:
### Repository Structure
- **Root**: Monorepo configuration with pnpm workspaces
- **packages/zod/**: Main Zod package with multiple version exports
- **packages/docs/**: Documentation website (Next.js)
- **packages/bench/**: Performance benchmarks
- **packages/resolution/**: Module resolution testing
- **packages/treeshake/**: Bundle size analysis
- **packages/tsc/**: TypeScript compilation benchmarks
### Version Architecture
The main zod package exports multiple versions:
1. **v4 (default)**: Current version, exports from `v4/classic/external.js`
2. **v4/core**: Core v4 implementation without legacy compatibility
3. **v4/mini**: Lightweight v4 variant
4. **v3**: Legacy version for backward compatibility
5. **mini**: General minimal build
### Key Implementation Files
- `src/v4/core/`: Core validation logic, schemas, parsing, and error handling
- `src/v4/classic/`: v4 with legacy compatibility layer
- `src/v4/mini/`: Minimal v4 implementation
- `src/v3/`: Legacy v3 implementation
- `src/locales/`: Internationalization support
### Export Strategy
The package uses conditional exports with:
- `@zod/source`: Development condition pointing to TypeScript source
- Standard ESM/CJS exports for distribution
- Multiple entry points for different versions and variants
## Code Standards
### Linting and Formatting
- Uses Biome for both linting and formatting
- Line width: 120 characters
- Trailing commas: ES5 style for JavaScript, none for JSON
- Notable lint rule relaxations:
- `noExplicitAny: "off"` - `any` is allowed
- `noParameterAssign: "off"` - Required for performance optimizations
- `noNonNullAssertion: "off"` - Non-null assertions are allowed
### TypeScript Configuration
- Strict mode enabled with exact optional property types
- Node.js module resolution (NodeNext)
- Target: ES2020
- Custom conditions support for `@zod/source`
## Development Workflow
1. Use `play.ts` for initial experimentation with `pnpm dev play.ts`
2. Write tests in appropriate `tests/` directories
3. Build with `pnpm build` before testing changes
4. Run linting/formatting with `pnpm fix`
5. All changes must pass tests and type checking
## Build System
- Uses `zshy` build tool for the main package
- Generates both ES modules and CommonJS outputs
- Supports source maps and declaration files
- Post-build formatting with Biome
## Performance Considerations
- Performance is critical - parameter reassignment is allowed for optimization
- Benchmarks available in `packages/bench/`
- Bundle size monitoring in `packages/treeshake/`
- TypeScript compilation performance tracked in `packages/tsc/`
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+21 -4
View File
@@ -259,6 +259,7 @@ z.cuid2();
z.ulid();
z.ipv4();
z.ipv6();
z.mac();
z.cidrv4(); // ipv4 CIDR block
z.cidrv6(); // ipv6 CIDR block
z.hash("sha256"); // or "sha1", "sha384", "sha512", "md5"
@@ -487,10 +488,10 @@ Use the `precision` parameter to constrain the allowable decimal precision.
```ts
z.iso.time({ precision: -1 }); // HH:MM (minute precision)
z.iso.time({ precision: 0 }); // HH:MM:SS (second precision)
z.iso.time({ precision: 1 }); // HH:MM:SS.s (decisecond precision)
z.iso.time({ precision: 2 }); // HH:MM:SS.ss (centisecond precision)
z.iso.time({ precision: 3 }); // HH:MM:SS.sss (millisecond precision)
z.iso.time({ precision: 0 }); // HH:MM:SS (second precision)
z.iso.time({ precision: 1 }); // HH:MM:SS.s (decisecond precision)
z.iso.time({ precision: 2 }); // HH:MM:SS.ss (centisecond precision)
z.iso.time({ precision: 3 }); // HH:MM:SS.sss (millisecond precision)
```
### IP addresses
@@ -515,6 +516,22 @@ const cidrv6 = z.string().cidrv6();
cidrv6.parse("2001:db8::/32"); // ✅
```
### MAC Addresses
Validate standard 48-bit MAC address [IEEE 802](https://en.wikipedia.org/wiki/MAC_address).
```ts
const mac = z.mac();
mac.parse("00:1A:2B:3C:4D:5E"); // ✅
mac.parse("00-1a-2b-3c-4d-5e"); // ❌ colon-delimited by default
mac.parse("001A:2B3C:4D5E"); // ❌ standard formats only
mac.parse("00:1A:2b:3C:4d:5E"); // ❌ no mixed case
// custom delimiter
const dashMac = z.mac({ delimiter: "-" });
dashMac.parse("00-1A-2B-3C-4D-5E"); // ✅
```
### JWTs
Validate [JSON Web Tokens](https://jwt.io/).
+1
View File
@@ -81,6 +81,7 @@ z.cuid2();
z.ulid();
z.cidrv4();
z.cidrv6();
z.mac();
```
## Numeric types
+13
View File
@@ -614,6 +614,19 @@ export function ipv4(params?: string | core.$ZodIPv4Params): ZodIPv4 {
return core._ipv4(ZodIPv4, params);
}
// ZodMAC
export interface ZodMAC extends ZodStringFormat<"mac"> {
_zod: core.$ZodMACInternals;
}
export const ZodMAC: core.$constructor<ZodMAC> = /*@__PURE__*/ core.$constructor("ZodMAC", (inst, def) => {
// ZodStringFormat.init(inst, def);
core.$ZodMAC.init(inst, def);
ZodStringFormat.init(inst, def);
});
export function mac(params?: string | core.$ZodMACParams): ZodMAC {
return core._mac(ZodMAC, params);
}
// ZodIPv6
export interface ZodIPv6 extends ZodStringFormat<"ipv6"> {
_zod: core.$ZodIPv6Internals;
@@ -195,6 +195,28 @@ test("continuability", () => {
},
]
`);
expect(
z
.mac()
.refine(() => false)
.safeParse("invalid_value").error!.issues
).toMatchInlineSnapshot(`
[
{
"code": "invalid_format",
"format": "mac",
"message": "Invalid MAC address",
"origin": "string",
"path": [],
"pattern": "/^(([0-9A-F]{2}(:)[0-9A-F]{2}(\\3[0-9A-F]{2}){4})|([0-9a-f]{2}(:)[0-9a-f]{2}(\\6[0-9a-f]{2}){4}))$/",
},
{
"code": "custom",
"message": "Invalid input",
"path": [],
},
]
`);
expect(
z
.emoji()
@@ -775,6 +775,8 @@ test("format", () => {
expect(z.string().date().format).toEqual("date");
expect(z.string().time().format).toEqual("time");
expect(z.string().duration().format).toEqual("duration");
expect(z.mac().format).toEqual("mac");
});
test("min max getters", () => {
@@ -885,6 +887,58 @@ test("IPv6 validation", () => {
expect(() => ipv6.parse("254.164.77.1")).toThrow();
});
test("MAC validation", () => {
const mac = z.mac();
// Valid MAC addresses
expect(mac.safeParse("00:1A:2B:3C:4D:5E").success).toBe(true);
expect(mac.safeParse("FF:FF:FF:FF:FF:FF").success).toBe(true);
expect(mac.safeParse("00:11:22:33:44:55").success).toBe(true);
expect(mac.safeParse("A1:B2:C3:D4:E5:F6").success).toBe(true);
expect(mac.safeParse("10:20:30:40:50:60").success).toBe(true);
expect(mac.safeParse("0a:1b:2c:3d:4e:5f").success).toBe(true);
expect(mac.safeParse("12:34:56:78:9A:BC").success).toBe(true);
// Invalid MAC addresses
expect(mac.safeParse("00:1A-2B:3C-4D:5E").success).toBe(false);
expect(mac.safeParse("00:1A:2B:3C:4D").success).toBe(false);
expect(mac.safeParse("00:1A:2B:3C:4D").success).toBe(false);
expect(mac.safeParse("00-1A-2B-3C-4D").success).toBe(false);
expect(mac.safeParse("01-23-45-67-89-AB").success).toBe(false); // Dash delimiter not accepted by default
expect(mac.safeParse("AA-BB-CC-DD-EE-FF").success).toBe(false); // Dash delimiter not accepted by default
expect(mac.safeParse("DE-AD-BE-EF-00-01").success).toBe(false); // Dash delimiter not accepted by default
expect(mac.safeParse("98-76-54-32-10-FF").success).toBe(false); // Dash delimiter not accepted by default
expect(mac.safeParse("00:1A:2B:3C:4D:GZ").success).toBe(false);
expect(mac.safeParse("00:1A:2B:3C:4D:5E:GG").success).toBe(false);
expect(mac.safeParse("123:45:67:89:AB:CD").success).toBe(false);
expect(mac.safeParse("00--1A:2B:3C:4D:5E").success).toBe(false);
expect(mac.safeParse("00:1A::2B:3C:4D:5E").success).toBe(false);
expect(mac.safeParse("00:1A:2B:3C:3C:2B:1A:00").success).toBe(false); // Disallow EUI-64
expect(mac.safeParse("00:1a:2B:3c:4D:5e").success).toBe(false); // Disallow mixed-case
// MAC formats that are nonstandard but occassionally referenced, ex. https://www.postgresql.org/docs/17/datatype-net-types.html#DATATYPE-MACADDR
expect(mac.safeParse("00:1A:2B:3C:4D:5E:FF").success).toBe(false);
expect(mac.safeParse("001A2B:3C4D5E").success).toBe(false);
expect(mac.safeParse("001A:2B3C:4D5E").success).toBe(false);
expect(mac.safeParse("001A.2B3C.4D5E").success).toBe(false);
expect(mac.safeParse("001A2B3C4D5E").success).toBe(false);
expect(mac.safeParse("00.1A.2B.3C.4D.5E").success).toBe(false);
});
test("MAC validation with custom delimiter", () => {
const colonMac = z.mac({ delimiter: ":" });
expect(colonMac.safeParse("00:1A:2B:3C:4D:5E").success).toBe(true);
expect(colonMac.safeParse("00-1A-2B-3C-4D-5E").success).toBe(false);
const dashMac = z.mac({ delimiter: "-" });
expect(dashMac.safeParse("00-1A-2B-3C-4D-5E").success).toBe(true);
expect(dashMac.safeParse("00:1A:2B:3C:4D:5E").success).toBe(false);
const colonOnlyMac = z.mac({ delimiter: ":" });
expect(colonOnlyMac.safeParse("00:1A:2B:3C:4D:5E").success).toBe(true);
expect(colonOnlyMac.safeParse("00-1A-2B-3C-4D-5E").success).toBe(false);
});
test("CIDR v4 validation", () => {
const cidrV4 = z.string().cidrv4();
@@ -43,6 +43,7 @@ const email = z.templateLiteral(["", z.string().email()]);
// const ip = z.templateLiteral(["", z.string().ip()]);
const ipv4 = z.templateLiteral(["", z.string().ipv4()]);
const ipv6 = z.templateLiteral(["", z.string().ipv6()]);
const mac = z.templateLiteral(["", z.mac()]);
const ulid = z.templateLiteral(["", z.string().ulid()]);
const uuid = z.templateLiteral(["", z.string().uuid()]);
const stringAToZ = z.templateLiteral(["", z.string().regex(/^[a-z]+$/)]);
@@ -137,6 +138,7 @@ test("template literal type inference", () => {
// expectTypeOf<z.infer<typeof ip>>().toEqualTypeOf<string>();
expectTypeOf<z.infer<typeof ipv4>>().toEqualTypeOf<string>();
expectTypeOf<z.infer<typeof ipv6>>().toEqualTypeOf<string>();
expectTypeOf<z.infer<typeof mac>>().toEqualTypeOf<string>();
expectTypeOf<z.infer<typeof ulid>>().toEqualTypeOf<string>();
expectTypeOf<z.infer<typeof uuid>>().toEqualTypeOf<string>();
expectTypeOf<z.infer<typeof stringAToZ>>().toEqualTypeOf<string>();
@@ -361,6 +363,7 @@ test("template literal parsing - success - basic cases", () => {
// ip.parse("c359:f57c:21e5:39eb:1187:e501:f936:b452");
ipv4.parse("213.174.246.205");
ipv6.parse("c359:f57c:21e5:39eb:1187:e501:f936:b452");
mac.parse("00:1A:2B:3C:4D:5E");
ulid.parse("01GW3D2QZJBYB6P1Z1AE997VPW");
uuid.parse("808989fd-3a6e-4af2-b607-737323a176f6");
stringAToZ.parse("asudgaskhdgashd");
@@ -497,6 +500,8 @@ test("template literal parsing - failure - basic cases", () => {
expect(() => ipv4.parse("c359:f57c:21e5:39eb:1187:e501:f936:b452")).toThrow();
expect(() => ipv6.parse("c359:f57c:21e5:39eb:1187:e501:f936:b4521")).toThrow();
expect(() => ipv6.parse("213.174.246.205")).toThrow();
expect(() => mac.parse("00:1A:2B:3C:4D:5E:6A:7B")).toThrow();
expect(() => mac.parse("00:1A:2B:3C")).toThrow();
expect(() => ulid.parse("01GW3D2QZJBYB6P1Z1AE997VPW!")).toThrow();
expect(() => uuid.parse("808989fd-3a6e-4af2-b607-737323a176f6Z")).toThrow();
expect(() => uuid.parse("Z808989fd-3a6e-4af2-b607-737323a176f6")).toThrow();
@@ -568,6 +573,9 @@ test("regexes", () => {
expect(ipv6._zod.pattern.source).toMatchInlineSnapshot(
`"^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$"`
);
expect(mac._zod.pattern.source).toMatchInlineSnapshot(
`"^(([0-9A-F]{2}(:)[0-9A-F]{2}(\\3[0-9A-F]{2}){4})|([0-9a-f]{2}(:)[0-9a-f]{2}(\\6[0-9a-f]{2}){4}))$"`
);
expect(ulid._zod.pattern.source).toMatchInlineSnapshot(`"^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$"`);
expect(uuid._zod.pattern.source).toMatchInlineSnapshot(
`"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"`
@@ -132,6 +132,30 @@ describe("toJSONSchema", () => {
"type": "string",
}
`);
expect(z.toJSONSchema(z.mac())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "mac",
"pattern": "^(([0-9A-F]{2}(:)[0-9A-F]{2}(\\3[0-9A-F]{2}){4})|([0-9a-f]{2}(:)[0-9a-f]{2}(\\6[0-9a-f]{2}){4}))$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.mac({ delimiter: ":" }))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "mac",
"pattern": "^(([0-9A-F]{2}(:)[0-9A-F]{2}(\\3[0-9A-F]{2}){4})|([0-9a-f]{2}(:)[0-9a-f]{2}(\\6[0-9a-f]{2}){4}))$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.mac({ delimiter: "-" }))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "mac",
"pattern": "^(([0-9A-F]{2}(-)[0-9A-F]{2}(\\3[0-9A-F]{2}){4})|([0-9a-f]{2}(-)[0-9a-f]{2}(\\6[0-9a-f]{2}){4}))$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.uuid())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -357,6 +381,31 @@ describe("toJSONSchema", () => {
}
`);
expect(z.toJSONSchema(z.mac())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "mac",
"pattern": "^(([0-9A-F]{2}(:)[0-9A-F]{2}(\\3[0-9A-F]{2}){4})|([0-9a-f]{2}(:)[0-9a-f]{2}(\\6[0-9a-f]{2}){4}))$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.mac({ delimiter: ":" }))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "mac",
"pattern": "^(([0-9A-F]{2}(:)[0-9A-F]{2}(\\3[0-9A-F]{2}){4})|([0-9a-f]{2}(:)[0-9a-f]{2}(\\6[0-9a-f]{2}){4}))$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.mac({ delimiter: "-" }))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "mac",
"pattern": "^(([0-9A-F]{2}(-)[0-9A-F]{2}(\\3[0-9A-F]{2}){4})|([0-9a-f]{2}(-)[0-9a-f]{2}(\\6[0-9a-f]{2}){4}))$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.base64())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
+16
View File
@@ -345,6 +345,22 @@ export function _ipv6<T extends schemas.$ZodIPv6>(
});
}
// MAC
export type $ZodMACParams = StringFormatParams<schemas.$ZodMAC, "pattern" | "when">;
export type $ZodCheckMACParams = CheckStringFormatParams<schemas.$ZodMAC, "pattern" | "when">;
export function _mac<T extends schemas.$ZodMAC>(
Class: util.SchemaClass<T>,
params?: string | $ZodMACParams | $ZodCheckMACParams
): T {
return new Class({
type: "string",
format: "mac",
check: "string_format",
abort: false,
...util.normalizeParams(params),
});
}
// CIDRv4
export type $ZodCIDRv4Params = StringFormatParams<schemas.$ZodCIDRv4, "pattern" | "when">;
export type $ZodCheckCIDRv4Params = CheckStringFormatParams<schemas.$ZodCIDRv4, "pattern" | "when">;
+8 -1
View File
@@ -1,3 +1,5 @@
import * as util from "./util.js";
export const cuid: RegExp = /^[cC][^\s-]{8,}$/;
export const cuid2: RegExp = /^[0-9a-z]+$/;
export const ulid: RegExp = /^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/;
@@ -59,7 +61,12 @@ export const ipv4: RegExp =
/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
export const ipv6: RegExp =
/^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
export const mac = (delimiter?: string): RegExp => {
const escapedDelim = util.escapeRegex(delimiter ?? ":");
return new RegExp(
`^(([0-9A-F]{2}(${escapedDelim})[0-9A-F]{2}(\\3[0-9A-F]{2}){4})|([0-9a-f]{2}(${escapedDelim})[0-9a-f]{2}(\\6[0-9a-f]{2}){4}))$`
);
};
export const cidrv4: RegExp =
/^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
export const cidrv6: RegExp =
+21
View File
@@ -782,6 +782,26 @@ export const $ZodIPv6: core.$constructor<$ZodIPv6> = /*@__PURE__*/ core.$constru
};
});
////////////////////////////// ZodMAC //////////////////////////////
export interface $ZodMACDef extends $ZodStringFormatDef<"mac"> {
delimiter?: string;
}
export interface $ZodMACInternals extends $ZodStringFormatInternals<"mac"> {
def: $ZodMACDef;
}
export interface $ZodMAC extends $ZodType {
_zod: $ZodMACInternals;
}
export const $ZodMAC: core.$constructor<$ZodMAC> = /*@__PURE__*/ core.$constructor("$ZodMAC", (inst, def): void => {
def.pattern ??= regexes.mac(def.delimiter);
$ZodStringFormat.init(inst, def);
inst._zod.bag.format = `mac`;
});
////////////////////////////// ZodCIDRv4 //////////////////////////////
export interface $ZodCIDRv4Def extends $ZodStringFormatDef<"cidrv4"> {
@@ -4300,6 +4320,7 @@ export type $ZodStringFormatTypes =
| $ZodISODuration
| $ZodIPv4
| $ZodIPv6
| $ZodMAC
| $ZodCIDRv4
| $ZodCIDRv6
| $ZodBase64
+1
View File
@@ -60,6 +60,7 @@ const error: () => errors.$ZodErrorMap = () => {
duration: "ISO duration",
ipv4: "IPv4 address",
ipv6: "IPv6 address",
mac: "MAC address",
cidrv4: "IPv4 range",
cidrv6: "IPv6 range",
base64: "base64-encoded string",
+13
View File
@@ -366,6 +366,19 @@ export function cidrv6(params?: string | core.$ZodCIDRv6Params): ZodMiniCIDRv6 {
return core._cidrv6(ZodMiniCIDRv6, params);
}
// ZodMiniMAC
export interface ZodMiniMAC extends _ZodMiniString<core.$ZodMACInternals> {
// _zod: core.$ZodMACInternals;
}
export const ZodMiniMAC: core.$constructor<ZodMiniMAC> = /*@__PURE__*/ core.$constructor("ZodMiniMAC", (inst, def) => {
core.$ZodMAC.init(inst, def);
ZodMiniStringFormat.init(inst, def);
});
export function mac(params?: string | core.$ZodMACParams): ZodMiniMAC {
return core._mac(ZodMiniMAC, params);
}
// ZodMiniBase64
export interface ZodMiniBase64 extends _ZodMiniString<core.$ZodBase64Internals> {
// _zod: core.$ZodBase64Internals;
@@ -241,6 +241,38 @@ test("z.ipv6", () => {
expect(() => z.parse(a, 123)).toThrow();
});
test("z.mac", () => {
const a = z.mac();
// valid mac
expect(z.parse(a, "00:1A:2B:3C:4D:5E")).toEqual("00:1A:2B:3C:4D:5E");
// invalid mac (dash delimiter not accepted by default)
expect(() => z.parse(a, "01-23-45-67-89-AB")).toThrow();
expect(() => z.parse(a, "00:1A:2B::4D:5E")).toThrow();
expect(() => z.parse(a, "00:1a-2B:3c-4D:5e")).toThrow();
expect(() => z.parse(a, "hello")).toThrow();
// wrong type
expect(() => z.parse(a, 123)).toThrow();
});
test("z.mac with custom delimiter", () => {
const a = z.mac({ delimiter: ":" });
// valid mac with colon
expect(z.parse(a, "00:1A:2B:3C:4D:5E")).toEqual("00:1A:2B:3C:4D:5E");
// invalid mac with dash
expect(() => z.parse(a, "00-1A-2B-3C-4D-5E")).toThrow();
const b = z.mac({ delimiter: "-" });
// valid mac with dash
expect(z.parse(b, "00-1A-2B-3C-4D-5E")).toEqual("00-1A-2B-3C-4D-5E");
// invalid mac with colon
expect(() => z.parse(b, "00:1A:2B:3C:4D:5E")).toThrow();
const c = z.mac({ delimiter: ":" });
// colon-only mac
expect(z.parse(c, "00:1A:2B:3C:4D:5E")).toEqual("00:1A:2B:3C:4D:5E");
expect(() => z.parse(c, "00-1A-2B-3C-4D-5E")).toThrow();
});
test("z.base64", () => {
const a = z.base64();
// valid base64
+6 -25
View File
@@ -1,29 +1,10 @@
import * as z from "zod";
// Test the circular reference issue with z.lazy()
const schema = z.lazy(() => z.tuple([schema]));
z;
console.log("Testing circular lazy schema creation:");
try {
// Try to access properties that depend on innerType during construction
// This should trigger the error if the bug exists
const _pattern = schema._zod.pattern;
const _optin = schema._zod.optin;
console.log("✅ Schema created successfully");
console.log("Pattern:", _pattern);
console.log("Optin:", _optin);
} catch (error) {
console.log("❌ Error during schema creation:", error);
if (error instanceof Error) {
console.log("Message:", error.message);
console.log("Stack:", error.stack);
}
}
const mac = z.stringFormat(
"mac",
/^(([0-9A-F]{2}([-:])[0-9A-F]{2}(\3[0-9A-F]{2}){4})|([0-9a-f]{2}([-:])[0-9a-f]{2}(\6[0-9a-f]{2}){4}))$/
);
console.log("\nTesting circular lazy schema parsing:");
try {
const result = schema.parse([[]]);
console.log("✅ Parse Success:", result);
} catch (error) {
console.log("❌ Parse Error:", error);
}
mac.parse("00:1A:2B:3C:4D:5E");