* Fix File reference issues. Fixes #4880. Fixes #4933.

* Relax metadata constraint to object. Fixes #4929

* Clean up partialRecord implementation. Closes #4882.

* Fixes catch handling in object schemas. Fixes #4883.

* Add treeshake case

* Document optionality changes

* Fix docs build

* Do not return empty metadata objects. Fixed #4885

* Escape non-string literal patterns. Closes #4894

* Surface continuable errors preferentially in unions. Fixes #4896

* Drop private modifier. Fixes #4903

* Simplify object parsing. Closes #4900

* Clean up .caught

* Make message a non-getter. Closes #4901.

* Standardize behavior of transform and preprocess. Closes #4910

* Clean up docs

* Fix extend for recursive objects. Fixes #4931

* Make object methods work with recursive types. Closes #4931 again

* Improve laziness of object methods

* Make standardschema FailureResult work with prettifyError. Closes #4914.

* Do not use normalized URLs by default. Add normalize flag to ZodURL. Closes #4906

* Tweak URL docs. Closes #4959

* 4.0.6
This commit is contained in:
Colin McDonnell
2025-07-23 15:31:04 -07:00
committed by GitHub
parent 38fd3efe7f
commit 4fda7a42c9
42 changed files with 1078 additions and 354 deletions
-8
View File
@@ -1,8 +0,0 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
-11
View File
@@ -1,11 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "es2020",
"lib": ["es6", "dom"],
"lib": ["es6", "es2020"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"jsx": "react",
+111
View File
@@ -0,0 +1,111 @@
---
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.
+1
View File
@@ -0,0 +1 @@
# hoist-pattern[]=!@types/*
+2 -2
View File
@@ -18,9 +18,7 @@
"@arethetypeswrong/cli": "^0.17.4",
"@biomejs/biome": "^1.9.4",
"@types/benchmark": "^2.1.5",
"@types/node": "^20.17.30",
"@types/semver": "^7.7.0",
"@web-std/file": "^3.0.3",
"arktype": "^2.1.19",
"benchmark": "^2.1.4",
"chalk": "^5.4.1",
@@ -34,6 +32,8 @@
"mitata": "^0.1.14",
"prettier": "^3.5.3",
"recheck": "^4.5.0",
"@web-std/file": "^3.0.3",
"rolldown": "1.0.0-beta.18",
"rollup": "^4.39.0",
"semver": "^7.7.2",
+1 -1
View File
@@ -4,7 +4,7 @@ import { metabench } from "./metabench.js";
import * as z3 from "zod/v3";
import * as z4 from "zod/v4";
import * as z4lib from "zodnext/v4";
import * as z4lib from "zod4/v4";
const z3fields ={
+6 -6
View File
@@ -48,12 +48,12 @@ export default async function Page(props: {
// you can add other MDX components here
blockquote: Callout,
Tabs,
h1: (props) => <Heading as="h1" {...props} />,
h2: (props) => <Heading as="h2" {...props} />,
h3: (props) => <Heading as="h3" {...props} />,
h4: (props) => <Heading as="h4" {...props} />,
h5: (props) => <Heading as="h5" {...props} />,
h6: (props) => <Heading as="h6" {...props} />,
h1: (props: any) => <Heading as="h1" {...props} />,
h2: (props: any) => <Heading as="h2" {...props} />,
h3: (props: any) => <Heading as="h3" {...props} />,
h4: (props: any) => <Heading as="h4" {...props} />,
h5: (props: any) => <Heading as="h5" {...props} />,
h6: (props: any) => <Heading as="h6" {...props} />,
}}
/>
</DocsBody>
+7 -1
View File
@@ -317,7 +317,6 @@ const schema = z.url();
schema.parse("https://example.com"); // ✅
schema.parse("http://localhost"); // ✅
schema.parse("mailto:[email protected]"); // ✅
schema.parse("sup"); // ❌
```
As you can see this is quite permissive. Internally this uses the `new URL()` constructor to validate inputs; this behavior may differ across platforms and runtimes but it's the mostly rigorous way to validate URIs/URLs on any given JS runtime/engine.
@@ -357,6 +356,13 @@ schema.parse("http://example.com"); // ❌
```
</Callout>
To normalize URLs, use the `normalize` flag. This will overwrite the input value with the [normalized URL](https://chatgpt.com/share/6881547f-bebc-800f-9093-f5981e277c2c) returned by `new URL()`.
```ts
new URL("HTTP://ExAmPle.com:80/./a/../b?X=1#f oo").href
// => "http://example.com/b?X=1#f%20oo"
```
### ISO datetimes
As you may have noticed, Zod string includes a few date/time related validations. These validations are regular expression based, so they are not as strict as a full date/time library. However, they are very convenient for validating user input.
-1
View File
@@ -74,7 +74,6 @@ All other string formats are supported via `pattern`:
```ts
z.base64url();
z.cuid();
z.regex();
z.emoji();
z.nanoid();
z.cuid2();
+14
View File
@@ -409,6 +409,20 @@ schema.parse(undefined); // => 4
## `z.object()`
### defaults applied within optional fields
Defaults inside your properties are applied, even within optional fields. This aligns better with expectations and resolves a long-standing usability issue with Zod 3. This is a subtle change that may cause breakage in code paths that rely on key existence, etc.
```ts
const schema = z.object({
a: z.string().default("tuna").optional(),
});
schema.parse({});
// Zod 4: { a: "tuna" }
// Zod 3: {}
```
### deprecates `.strict()` and `.passthrough()`
These methods are generally no longer necessary. Instead use the top-level `z.strictObject()` and `z.looseObject()` functions.
+17
View File
@@ -7,6 +7,23 @@ export const config = {
runtime: "edge",
};
declare module 'react' {
interface HTMLAttributes<T> {
/**
* Specify styles using Tailwind CSS classes. This feature is currently experimental.
* If `style` prop is also specified, styles generated with `tw` prop will be overridden.
*
* Example:
* - `tw='w-full h-full bg-blue-200'`
* - `tw='text-9xl'`
* - `tw='text-[80px]'`
*
* @type {string}
*/
tw?: string;
}
}
// export const loadImage = (img: string) => {
// const p = path.join(process.cwd(), img);
// const image = fs.readFileSync(p);
+2 -1
View File
@@ -23,7 +23,8 @@
{
"name": "next"
}
]
],
"types": ["mdx", "node", "react", "react-dom", "@vercel/og"]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "components/if.tsx"],
"exclude": ["node_modules"]
@@ -0,0 +1,16 @@
import * as v from "valibot"; // 1.37 kB
const Valibot = v.object({
email: v.pipe(
v.string(),
v.nonEmpty("Please enter your email."),
v.email("The email address is badly formatted.")
),
password: v.pipe(
v.string(),
v.nonEmpty("Please enter your password."),
v.minLength(8, "Your password must have 8 characters or more.")
),
});
v.parse(Valibot, {});
@@ -0,0 +1,18 @@
import * as z from "zod/mini"; // 6.57 kB
const Zod = z.object({
email: z
.string()
.check(
z.minLength(1, "Please enter your email."),
z.email("The email address is badly formatted.")
),
password: z.string()
.check(
z.minLength(1, "Please enter your password."),
z.minLength(8, "Your password must have 8 characters or more.")
),
});
Zod.parse({});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@zod/zod",
"version": "4.0.5",
"version": "4.0.6",
"exports": {
"./package.json": "./package.json",
".": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "zod",
"version": "4.0.5",
"version": "4.0.6",
"type": "module",
"author": "Colin McDonnell <[email protected]>",
"description": "TypeScript-first schema declaration and validation library with static type inference",
+3 -1
View File
@@ -705,6 +705,7 @@ function isValidJWT(jwt: string, alg?: string): boolean {
.replace(/-/g, "+")
.replace(/_/g, "/")
.padEnd(header.length + ((4 - (header.length % 4)) % 4), "=");
// @ts-ignore
const decoded = JSON.parse(atob(base64));
if (typeof decoded !== "object" || decoded === null) return false;
if ("typ" in decoded && decoded?.typ !== "JWT") return false;
@@ -875,6 +876,7 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
}
} else if (check.kind === "url") {
try {
// @ts-ignore
new URL(input.data);
} catch {
ctx = this._getOrReturnCtx(input, ctx);
@@ -2454,7 +2456,7 @@ export class ZodObject<
Output = objectOutputType<T, Catchall, UnknownKeys>,
Input = objectInputType<T, Catchall, UnknownKeys>,
> extends ZodType<Output, ZodObjectDef<T, UnknownKeys, Catchall>, Input> {
private _cached: { shape: T; keys: string[] } | null = null;
_cached: { shape: T; keys: string[] } | null = null;
_getCached(): { shape: T; keys: string[] } {
if (this._cached !== null) return this._cached;
+9 -2
View File
@@ -1,5 +1,6 @@
import * as core from "../core/index.js";
import { $ZodError } from "../core/index.js";
import * as util from "../core/util.js";
/** @deprecated Use `z.core.$ZodIssue` from `@zod/core` instead, especially if you are building a library on top of Zod. */
export type ZodIssue = core.$ZodIssue;
@@ -34,11 +35,17 @@ const initializer = (inst: ZodError, issues: core.$ZodIssue[]) => {
// enumerable: false,
},
addIssue: {
value: (issue: any) => inst.issues.push(issue),
value: (issue: any) => {
inst.issues.push(issue);
inst.message = JSON.stringify(inst.issues, util.jsonStringifyReplacer, 2);
},
// enumerable: false,
},
addIssues: {
value: (issues: any) => inst.issues.push(...issues),
value: (issues: any) => {
inst.issues.push(...issues);
inst.message = JSON.stringify(inst.issues, util.jsonStringifyReplacer, 2);
},
// enumerable: false,
},
isEmpty: {
+10 -7
View File
@@ -1068,9 +1068,7 @@ export interface ZodObject<
/** This is the default behavior. This method call is likely unnecessary. */
strip(): ZodObject<Shape, core.$strip>;
extend<U extends core.$ZodLooseShape & Partial<Record<keyof Shape, core.SomeType>>>(
shape: U
): ZodObject<util.Extend<Shape, U>, Config>;
extend<U extends core.$ZodLooseShape>(shape: U): ZodObject<util.Extend<Shape, U>, Config>;
/**
* @deprecated Use [`A.extend(B.shape)`](https://zod.dev/api?id=extend) instead.
@@ -1130,7 +1128,6 @@ export const ZodObject: core.$constructor<ZodObject> = /*@__PURE__*/ core.$const
inst.keyof = () => _enum(Object.keys(inst._zod.def.shape)) as any;
inst.catchall = (catchall) => inst.clone({ ...inst._zod.def, catchall: catchall as any as core.$ZodType }) as any;
inst.passthrough = () => inst.clone({ ...inst._zod.def, catchall: unknown() });
// inst.nonstrict = () => inst.clone({ ...inst._zod.def, catchall: api.unknown() });
inst.loose = () => inst.clone({ ...inst._zod.def, catchall: unknown() });
inst.strict = () => inst.clone({ ...inst._zod.def, catchall: never() });
inst.strip = () => inst.clone({ ...inst._zod.def, catchall: undefined });
@@ -1350,9 +1347,11 @@ export function partialRecord<Key extends core.$ZodRecordKey, Value extends core
valueType: Value,
params?: string | core.$ZodRecordParams
): ZodRecord<Key & core.$partial, Value> {
const k = core.clone(keyType);
k._zod.values = undefined;
return new ZodRecord({
type: "record",
keyType: union([keyType, never()]),
keyType: k,
valueType: valueType as any,
...util.normalizeParams(params),
}) as any;
@@ -1584,7 +1583,7 @@ export const ZodTransform: core.$constructor<ZodTransform> = /*@__PURE__*/ core.
_issue.code ??= "custom";
_issue.input ??= payload.value;
_issue.inst ??= inst;
_issue.continue ??= true;
// _issue.continue ??= true;
payload.issues.push(util.issue(_issue));
}
};
@@ -1838,12 +1837,16 @@ export function pipe(in_: core.SomeType, out: core.SomeType) {
// ZodReadonly
export interface ZodReadonly<T extends core.SomeType = core.$ZodType>
extends _ZodType<core.$ZodReadonlyInternals<T>>,
core.$ZodReadonly<T> {}
core.$ZodReadonly<T> {
unwrap(): T;
}
export const ZodReadonly: core.$constructor<ZodReadonly> = /*@__PURE__*/ core.$constructor(
"ZodReadonly",
(inst, def) => {
core.$ZodReadonly.init(inst, def);
ZodType.init(inst, def);
inst.unwrap = () => inst._zod.def.innerType;
}
);
@@ -550,3 +550,46 @@ test("disc union treeify/format", () => {
}
`);
});
test("update message after adding issues", () => {
const e = new z.ZodError([]);
e.addIssue({
code: "custom",
message: "message",
input: "asdf",
path: [],
});
expect(e.message).toMatchInlineSnapshot(`
"[
{
"code": "custom",
"message": "message",
"input": "asdf",
"path": []
}
]"
`);
e.addIssue({
code: "custom",
message: "message",
input: "asdf",
path: [],
});
expect(e.message).toMatchInlineSnapshot(`
"[
{
"code": "custom",
"message": "message",
"input": "asdf",
"path": []
},
{
"code": "custom",
"message": "message",
"input": "asdf",
"path": []
}
]"
`);
});
@@ -145,3 +145,196 @@ test("partial with mask -- ignore falsy values", async () => {
masked.parse({ country: "US" });
await masked.parseAsync({ country: "US" });
});
test("catch/prefault/default", () => {
const mySchema = z.object({
a: z.string().catch("catch value").optional(),
b: z.string().default("default value").optional(),
c: z.string().prefault("prefault value").optional(),
d: z.string().catch("catch value"),
e: z.string().default("default value"),
f: z.string().prefault("prefault value"),
});
expect(mySchema.parse({})).toMatchInlineSnapshot(`
{
"b": "default value",
"c": "prefault value",
"d": "catch value",
"e": "default value",
"f": "prefault value",
}
`);
expect(mySchema.parse({}, { jitless: true })).toMatchInlineSnapshot(`
{
"b": "default value",
"c": "prefault value",
"d": "catch value",
"e": "default value",
"f": "prefault value",
}
`);
});
test("handleOptionalObjectResult branches", () => {
const mySchema = z.object({
// Branch: input[key] === undefined, key not in input, caught error
caughtMissing: z.string().catch("caught").optional(),
// Branch: input[key] === undefined, key in input, caught error
caughtUndefined: z.string().catch("caught").optional(),
// Branch: input[key] === undefined, key not in input, validation issues
issueMissing: z.string().min(5).optional(),
// Branch: input[key] === undefined, key in input, validation issues
issueUndefined: z.string().min(5).optional(),
// Branch: input[key] === undefined, validation returns undefined
validUndefined: z.string().optional(),
// Branch: input[key] === undefined, non-undefined result (default/transform)
defaultValue: z.string().default("default").optional(),
// Branch: input[key] defined, caught error
caughtDefined: z.string().catch("caught").optional(),
// Branch: input[key] defined, validation issues
issueDefined: z.string().min(5).optional(),
// Branch: input[key] defined, validation returns undefined
validDefinedUndefined: z
.string()
.transform(() => undefined)
.optional(),
// Branch: input[key] defined, non-undefined value
validDefined: z.string().optional(),
});
// Test input[key] === undefined cases
const result1 = mySchema.parse(
{
// caughtMissing: not present (key not in input)
caughtUndefined: undefined, // key in input
// issueMissing: not present (key not in input)
issueUndefined: undefined, // key in input
validUndefined: undefined,
// defaultValue: not present, will get default
},
{ jitless: true }
);
expect(result1).toEqual({
caughtUndefined: undefined,
issueUndefined: undefined,
validUndefined: undefined,
defaultValue: "default",
});
// Test input[key] defined cases (successful)
const result2 = mySchema.parse(
{
caughtDefined: 123, // invalid type, should catch
validDefinedUndefined: "test", // transforms to undefined
validDefined: "valid", // valid value
},
{ jitless: true }
);
expect(result2).toEqual({
caughtDefined: "caught",
validDefinedUndefined: undefined,
validDefined: "valid",
defaultValue: "default",
});
// Test validation issues are properly reported (input[key] defined, validation fails)
expect(() =>
mySchema.parse(
{
issueDefined: "abc", // too short
},
{ jitless: true }
)
).toThrow();
});
test("fastpass vs non-fastpass consistency", () => {
const mySchema = z.object({
caughtMissing: z.string().catch("caught").optional(),
caughtUndefined: z.string().catch("caught").optional(),
issueMissing: z.string().min(5).optional(),
issueUndefined: z.string().min(5).optional(),
validUndefined: z.string().optional(),
defaultValue: z.string().default("default").optional(),
caughtDefined: z.string().catch("caught").optional(),
validDefinedUndefined: z
.string()
.transform(() => undefined)
.optional(),
validDefined: z.string().optional(),
});
const input = {
caughtUndefined: undefined,
issueUndefined: undefined,
validUndefined: undefined,
caughtDefined: 123,
validDefinedUndefined: "test",
validDefined: "valid",
};
// Test both paths produce identical results
const jitlessResult = mySchema.parse(input, { jitless: true });
const fastpassResult = mySchema.parse(input);
expect(jitlessResult).toEqual(fastpassResult);
expect(jitlessResult).toEqual({
caughtUndefined: undefined,
issueUndefined: undefined,
validUndefined: undefined,
defaultValue: "default",
caughtDefined: "caught",
validDefinedUndefined: undefined,
validDefined: "valid",
});
});
test("optional with check", () => {
const baseSchema = z
.string()
.optional()
.check(({ value, ...ctx }) => {
ctx.issues.push({
code: "custom",
input: value,
message: "message",
});
});
// this correctly fails
expect(baseSchema.safeParse(undefined)).toMatchInlineSnapshot(`
{
"error": [ZodError: [
{
"code": "custom",
"message": "message",
"path": []
}
]],
"success": false,
}
`);
const schemaObject = z.object({
date: baseSchema,
});
expect(schemaObject.safeParse({ date: undefined })).toMatchInlineSnapshot(`
{
"error": [ZodError: [
{
"code": "custom",
"message": "message",
"path": [
"date"
]
}
]],
"success": false,
}
`);
});
@@ -114,14 +114,14 @@ test("pick/omit/required/partial - do not allow unknown keys", () => {
age: z.number(),
});
expect(() => schema.pick({ name: true, asdf: true })).toThrow();
expect(() => schema.pick({ name: true, asdf: true }).safeParse({})).toThrow();
// @ts-expect-error
expect(() => schema.pick({ $unknown: true })).toThrow();
expect(() => schema.pick({ $unknown: true }).safeParse({})).toThrow();
// @ts-expect-error
expect(() => schema.omit({ $unknown: true })).toThrow();
expect(() => schema.omit({ $unknown: true }).safeParse({})).toThrow();
// @ts-expect-error
expect(() => schema.required({ $unknown: true })).toThrow();
expect(() => schema.required({ $unknown: true }).safeParse({})).toThrow();
// @ts-expect-error
expect(() => schema.partial({ $unknown: true })).toThrow();
expect(() => schema.partial({ $unknown: true }).safeParse({})).toThrow();
});
@@ -73,17 +73,18 @@ test("preprocess ctx.addIssue with parse", () => {
`);
});
test("preprocess ctx.addIssue non-fatal by default", () => {
test("preprocess ctx.addIssue fatal by default", () => {
const schema = z.preprocess((data, ctx) => {
ctx.addIssue({
code: "custom",
message: `custom error`,
});
return data;
}, z.string());
const result = schema.safeParse(1234);
expect(result.error!.issues).toHaveLength(2);
expect(result.error!.issues).toHaveLength(1);
expect(result).toMatchInlineSnapshot(`
{
"error": [ZodError: [
@@ -91,12 +92,6 @@ test("preprocess ctx.addIssue non-fatal by default", () => {
"code": "custom",
"message": "custom error",
"path": []
},
{
"expected": "string",
"code": "invalid_type",
"path": [],
"message": "Invalid input: expected string, received number"
}
]],
"success": false,
@@ -175,7 +170,7 @@ test("z.NEVER in preprocess", () => {
expectTypeOf<foo>().toEqualTypeOf<number>();
const result = foo.safeParse(undefined);
expect(result.error!.issues).toHaveLength(2);
expect(result.error!.issues).toHaveLength(1);
expect(result).toMatchInlineSnapshot(`
{
"error": [ZodError: [
@@ -183,12 +178,6 @@ test("z.NEVER in preprocess", () => {
"code": "custom",
"message": "bad",
"path": []
},
{
"expected": "number",
"code": "invalid_type",
"path": [],
"message": "Invalid input: expected number, received object"
}
]],
"success": false,
@@ -336,7 +336,21 @@ test("partial record", () => {
type schema = z.infer<typeof schema>;
expectTypeOf<schema>().toEqualTypeOf<Partial<Record<string, string>>>();
const Keys = z.enum(["id", "name", "email"]).or(z.never());
const Keys = z.enum(["id", "name", "email"]); //.or(z.never());
const Person = z.partialRecord(Keys, z.string());
expectTypeOf<z.infer<typeof Person>>().toEqualTypeOf<Partial<Record<"id" | "name" | "email", string>>>();
Person.parse({
id: "123",
// name: "John",
// email: "[email protected]",
});
Person.parse({
// id: "123",
// name: "John",
email: "[email protected]",
});
expect(Person.def.keyType._zod.def.type).toEqual("enum");
});
@@ -260,6 +260,73 @@ test("mutual recursion with meta", () => {
expectTypeOf<B>().toEqualTypeOf<_B>();
});
test("object utilities with recursive types", () => {
const NodeBase = z.object({
id: z.string(),
name: z.string(),
get children() {
return z.array(Node).optional();
},
});
// Test extend
const NodeOne = NodeBase.extend({
name: z.literal("nodeOne"),
get children() {
return z.array(Node);
},
});
const NodeTwo = NodeBase.extend({
name: z.literal("nodeTwo"),
get children() {
return z.array(Node);
},
});
// Test pick
const PickedNode = NodeBase.pick({ id: true, name: true });
// Test omit
const OmittedNode = NodeBase.omit({ children: true });
// Test merge
const ExtraProps = {
metadata: z.string(),
get parent() {
return Node.optional();
},
};
const MergedNode = NodeBase.extend(ExtraProps);
// Test partial
const PartialNode = NodeBase.partial();
const PartialMaskedNode = NodeBase.partial({ name: true });
// Test required (assuming NodeBase has optional fields)
const OptionalNodeBase = z.object({
id: z.string().optional(),
name: z.string().optional(),
get children() {
return z.array(Node).optional();
},
});
const RequiredNode = OptionalNodeBase.required();
const RequiredMaskedNode = OptionalNodeBase.required({ id: true });
const Node = z.union([
NodeOne,
NodeTwo,
PickedNode,
OmittedNode,
MergedNode,
PartialNode,
PartialMaskedNode,
RequiredNode,
RequiredMaskedNode,
]);
});
test("recursion compatibility", () => {
// array
const A = z.object({
@@ -318,6 +318,83 @@ test("url validations", () => {
expect(() => url.parse("https://")).toThrow();
});
test("url preserves original input", () => {
const url = z.string().url();
// Test the specific case from the user report
const input = "https://example.com?key=NUXOmHqWNVTapJkJJHw8BfD155AuqhH_qju_5fNmQ4ZHV7u8";
const output = url.parse(input);
expect(output).toBe(input); // Should preserve the original input exactly
// Test other cases where URL constructor would normalize
expect(url.parse("https://example.com?foo=bar")).toBe("https://example.com?foo=bar");
expect(url.parse("http://example.com?test=123")).toBe("http://example.com?test=123");
expect(url.parse("https://sub.example.com?param=value&other=data")).toBe(
"https://sub.example.com?param=value&other=data"
);
// Test cases with trailing slashes are preserved
expect(url.parse("https://example.com/")).toBe("https://example.com/");
expect(url.parse("https://example.com/path/")).toBe("https://example.com/path/");
// Test cases with paths and query parameters
expect(url.parse("https://example.com/path?query=param")).toBe("https://example.com/path?query=param");
});
test("url trims whitespace", () => {
const url = z.string().url();
// Test trimming whitespace from URLs
expect(url.parse(" https://example.com ")).toBe("https://example.com");
expect(url.parse(" https://example.com/path?query=param ")).toBe("https://example.com/path?query=param");
expect(url.parse("\t\nhttps://example.com\t\n")).toBe("https://example.com");
expect(url.parse(" https://example.com?key=value ")).toBe("https://example.com?key=value");
// Test that URLs without extra whitespace are unchanged
expect(url.parse("https://example.com")).toBe("https://example.com");
expect(url.parse("https://example.com/path")).toBe("https://example.com/path");
});
test("url normalize flag", () => {
const normalizeUrl = z.url({ normalize: true });
const preserveUrl = z.url(); // normalize: false/undefined by default
// Test that normalize flag causes URL normalization
expect(normalizeUrl.parse("https://example.com?key=value")).toBe("https://example.com/?key=value");
expect(normalizeUrl.parse("http://example.com?test=123")).toBe("http://example.com/?test=123");
// Test with already normalized URLs
expect(normalizeUrl.parse("https://example.com/")).toBe("https://example.com/");
expect(normalizeUrl.parse("https://example.com/path?query=param")).toBe("https://example.com/path?query=param");
// Test complex URLs with normalization
expect(normalizeUrl.parse("https://example.com/../?key=value")).toBe("https://example.com/?key=value");
expect(normalizeUrl.parse("https://example.com/./path?key=value")).toBe("https://example.com/path?key=value");
// Compare with non-normalize behavior
expect(preserveUrl.parse("https://example.com?key=value")).toBe("https://example.com?key=value");
expect(preserveUrl.parse("http://example.com?test=123")).toBe("http://example.com?test=123");
// Test trimming with normalize
expect(normalizeUrl.parse(" https://example.com?key=value ")).toBe("https://example.com/?key=value");
expect(preserveUrl.parse(" https://example.com?key=value ")).toBe("https://example.com?key=value");
});
test("url normalize with hostname and protocol constraints", () => {
const constrainedNormalizeUrl = z.url({
normalize: true,
protocol: /^https$/,
hostname: /^example\.com$/,
});
// Test that normalization works with constraints
expect(constrainedNormalizeUrl.parse("https://example.com?key=value")).toBe("https://example.com/?key=value");
// Test that constraints are still enforced
expect(() => constrainedNormalizeUrl.parse("http://example.com?key=value")).toThrow();
expect(() => constrainedNormalizeUrl.parse("https://other.com?key=value")).toThrow();
});
test("httpurl", () => {
const httpUrl = z.url({
protocol: /^https?$/,
@@ -1952,6 +1952,7 @@ test("input type", () => {
"required": [
"a",
"d",
"f",
"g",
],
"type": "object",
@@ -248,3 +248,113 @@ test("async short circuit on dirty", async () => {
]]
`);
});
test("do not continue by default", () => {
const A = z
.string()
.transform((val, ctx) => {
ctx.addIssue({
code: "custom",
message: `custom error`,
});
ctx.addIssue({
code: "custom",
message: `custom error`,
});
return val;
})
.pipe(z.number() as any);
expect(A.safeParse("asdf")).toMatchInlineSnapshot(`
{
"error": [ZodError: [
{
"code": "custom",
"message": "custom error",
"path": []
},
{
"code": "custom",
"message": "custom error",
"path": []
}
]],
"success": false,
}
`);
const B = z
.string()
.transform((val, ctx) => {
ctx.issues.push({
code: "custom",
message: `custom error`,
input: val,
});
ctx.issues.push({
code: "custom",
message: `custom error`,
input: val,
});
return val;
})
.pipe(z.number() as any);
expect(B.safeParse("asdf")).toMatchInlineSnapshot(`
{
"error": [ZodError: [
{
"code": "custom",
"message": "custom error",
"path": []
},
{
"code": "custom",
"message": "custom error",
"path": []
}
]],
"success": false,
}
`);
const C = z
.string()
.transform((val, ctx) => {
ctx.issues.push({
code: "custom",
message: `custom error`,
input: val,
continue: true,
});
ctx.issues.push({
code: "custom",
message: `custom error`,
input: val,
continue: true,
});
return val;
})
.pipe(z.number() as any);
expect(C.safeParse("asdf")).toMatchInlineSnapshot(`
{
"error": [ZodError: [
{
"code": "custom",
"message": "custom error",
"path": []
},
{
"code": "custom",
"message": "custom error",
"path": []
},
{
"expected": "number",
"code": "invalid_type",
"path": [],
"message": "Invalid input: expected number, received string"
}
]],
"success": false,
}
`);
});
@@ -27,7 +27,7 @@ test("return valid over invalid", () => {
});
test("return errors from both union arms", () => {
const result = z.union([z.number(), z.string().refine(() => false)]).safeParse("a");
const result = z.union([z.number(), z.boolean()]).safeParse("a");
expect(result.success).toEqual(false);
if (!result.success) {
expect(result.error.issues).toMatchInlineSnapshot(`
@@ -45,8 +45,9 @@ test("return errors from both union arms", () => {
],
[
{
"code": "custom",
"message": "Invalid input",
"code": "invalid_type",
"expected": "boolean",
"message": "Invalid input: expected boolean, received string",
"path": [],
},
],
@@ -92,3 +93,44 @@ test("union values", () => {
}
`);
});
test("non-aborted errors", () => {
const zItemTest = z.union([
z.object({
date: z.number(),
startDate: z.optional(z.null()),
endDate: z.optional(z.null()),
}),
z
.object({
date: z.optional(z.null()),
startDate: z.number(),
endDate: z.number(),
})
.refine((data) => data.startDate !== data.endDate, {
error: "startDate and endDate must be different",
path: ["endDate"],
}),
]);
const res = zItemTest.safeParse({
date: null,
startDate: 1,
endDate: 1,
});
expect(res).toMatchInlineSnapshot(`
{
"error": [ZodError: [
{
"code": "custom",
"path": [
"endDate"
],
"message": "startDate and endDate must be different"
}
]],
"success": false,
}
`);
});
+2 -2
View File
@@ -1128,12 +1128,12 @@ export interface $ZodCheckMimeTypeDef extends $ZodCheckDef {
mime: util.MimeTypes[];
}
export interface $ZodCheckMimeTypeInternals<T extends File = File> extends $ZodCheckInternals<T> {
export interface $ZodCheckMimeTypeInternals<T extends schemas.File = schemas.File> extends $ZodCheckInternals<T> {
def: $ZodCheckMimeTypeDef;
issc: errors.$ZodIssueInvalidValue;
}
export interface $ZodCheckMimeType<T extends File = File> extends $ZodCheck<T> {
export interface $ZodCheckMimeType<T extends schemas.File = schemas.File> extends $ZodCheck<T> {
_zod: $ZodCheckMimeTypeInternals<T>;
}
+8 -15
View File
@@ -1,6 +1,7 @@
import type { $ZodCheck, $ZodStringFormats } from "./checks.js";
import { $constructor } from "./core.js";
import type { $ZodType } from "./schemas.js";
import type { StandardSchemaV1 } from "./standard-schema.js";
import * as util from "./util.js";
///////////////////////////
@@ -162,7 +163,7 @@ type RawIssue<T extends $ZodIssueBase> = util.Flatten<
readonly input?: unknown;
/** The schema or check that originated this issue. */
readonly inst?: $ZodType | $ZodCheck;
/** @deprecated Internal use only. If `true`, Zod will continue executing validation despite this issue. */
/** If `true`, Zod will continue executing validation despite this issue. */
readonly continue?: boolean | undefined;
} & Record<string, any>
>;
@@ -196,13 +197,8 @@ const initializer = (inst: $ZodError, def: $ZodIssue[]): void => {
value: def,
enumerable: false,
});
Object.defineProperty(inst, "message", {
get() {
return JSON.stringify(def, util.jsonStringifyReplacer, 2);
},
enumerable: true,
// configurable: false,
});
inst.message = JSON.stringify(def, util.jsonStringifyReplacer, 2);
Object.defineProperty(inst, "toString", {
value: () => inst.message,
enumerable: false,
@@ -391,8 +387,9 @@ export function treeifyError<T>(error: $ZodError, _mapper?: any) {
* ✖ Invalid input: expected number
* ```
*/
export function toDotPath(path: (string | number | symbol)[]): string {
export function toDotPath(_path: readonly (string | number | symbol | StandardSchemaV1.PathSegment)[]): string {
const segs: string[] = [];
const path: PropertyKey[] = _path.map((seg: any) => (typeof seg === "object" ? seg.key : seg));
for (const seg of path) {
if (typeof seg === "number") segs.push(`[${seg}]`);
else if (typeof seg === "symbol") segs.push(`[${JSON.stringify(String(seg))}]`);
@@ -406,14 +403,10 @@ export function toDotPath(path: (string | number | symbol)[]): string {
return segs.join("");
}
interface BaseError {
issues: $ZodIssueBase[];
}
export function prettifyError(error: BaseError): string {
export function prettifyError(error: StandardSchemaV1.FailureResult): string {
const lines: string[] = [];
// sort by path length
const issues = [...error.issues].sort((a, b) => a.path.length - b.path.length);
const issues = [...error.issues].sort((a, b) => (a.path ?? []).length - (b.path ?? []).length);
// Process each issue
for (const issue of issues) {
+3 -2
View File
@@ -23,7 +23,7 @@ export type $replace<Meta, S extends $ZodType> = Meta extends $output
? { [K in keyof Meta]: $replace<Meta[K], S> }
: Meta;
type MetadataType = Record<string, unknown> | undefined;
type MetadataType = object | undefined;
export class $ZodRegistry<Meta extends MetadataType = MetadataType, Schema extends $ZodType = $ZodType> {
_meta!: Meta;
_schema!: Schema;
@@ -68,7 +68,8 @@ export class $ZodRegistry<Meta extends MetadataType = MetadataType, Schema exten
if (p) {
const pm: any = { ...(this.get(p) ?? {}) };
delete pm.id; // do not inherit id
return { ...pm, ...this._map.get(schema) } as any;
const f = { ...pm, ...this._map.get(schema) } as any;
return Object.keys(f).length ? f : undefined;
}
return this._map.get(schema) as any;
}
+88 -93
View File
@@ -121,6 +121,9 @@ export interface _$ZodTypeInternals {
*/
values?: util.PrimitiveSet | undefined;
/** Default value bubbled up from */
// default?: unknown | undefined;
/** @internal A set of literal discriminators used for the fast path in discriminated unions. */
propValues?: util.PropValues | undefined;
@@ -414,6 +417,7 @@ export const $ZodEmail: core.$constructor<$ZodEmail> = /*@__PURE__*/ core.$const
export interface $ZodURLDef extends $ZodStringFormatDef<"url"> {
hostname?: RegExp | undefined;
protocol?: RegExp | undefined;
normalize?: boolean | undefined;
}
export interface $ZodURLInternals extends $ZodStringFormatInternals<"url"> {
def: $ZodURLDef;
@@ -427,9 +431,10 @@ export const $ZodURL: core.$constructor<$ZodURL> = /*@__PURE__*/ core.$construct
$ZodStringFormat.init(inst, def);
inst._zod.check = (payload) => {
try {
const orig = payload.value;
const url = new URL(orig);
const href = url.href;
// Trim whitespace from input
const trimmed = payload.value.trim();
// @ts-ignore
const url = new URL(trimmed);
if (def.hostname) {
def.hostname.lastIndex = 0;
@@ -461,11 +466,13 @@ export const $ZodURL: core.$constructor<$ZodURL> = /*@__PURE__*/ core.$construct
}
}
// payload.value = url.href;
if (!orig.endsWith("/") && href.endsWith("/")) {
payload.value = href.slice(0, -1);
// Set the output value based on normalize flag
if (def.normalize) {
// Use normalized URL
payload.value = url.href;
} else {
payload.value = href;
// Preserve the original input (trimmed)
payload.value = trimmed;
}
return;
@@ -719,6 +726,7 @@ export const $ZodIPv6: core.$constructor<$ZodIPv6> = /*@__PURE__*/ core.$constru
inst._zod.check = (payload) => {
try {
// @ts-ignore
new URL(`http://[${payload.value}]`);
// return;
} catch {
@@ -782,6 +790,7 @@ export const $ZodCIDRv6: core.$constructor<$ZodCIDRv6> = /*@__PURE__*/ core.$con
const prefixNum = Number(prefix);
if (`${prefixNum}` !== prefix) throw new Error();
if (prefixNum < 0 || prefixNum > 128) throw new Error();
// @ts-ignore
new URL(`http://[${address}]`);
} catch {
payload.issues.push({
@@ -801,6 +810,7 @@ export function isValidBase64(data: string): boolean {
if (data === "") return true;
if (data.length % 4 !== 0) return false;
try {
// @ts-ignore
atob(data);
return true;
} catch {
@@ -900,6 +910,7 @@ export function isValidJWT(token: string, algorithm: util.JWTAlgorithm | null =
if (tokensParts.length !== 3) return false;
const [header] = tokensParts;
if (!header) return false;
// @ts-ignore
const parsedHeader = JSON.parse(atob(header));
if ("typ" in parsedHeader && parsedHeader?.typ !== "JWT") return false;
if (!parsedHeader.alg) return false;
@@ -1582,6 +1593,36 @@ export type $InferObjectOutput<T extends $ZodLooseShape, Extra extends Record<st
} & Extra
>;
// experimental
// export type $InferObjectOutput<T extends $ZodLooseShape, Extra extends Record<string, unknown>> = keyof (T &
// Extra) extends never
// ? Record<string, never>
// : string extends keyof T
// ? util.Prettify<
// {
// [k: string]: util.IsAny<T[string]["_zod"]["output"]> extends true ? unknown : T[string]["_zod"]["output"];
// } & $InferObjectOutputNoIndex<util.OmitIndexSignature<T>, Extra>
// >
// : util.Prettify<$InferObjectOutputNoIndex<T, Extra>>;
// export type $InferObjectOutputNoIndex<T extends $ZodLooseShape, Extra extends Record<string, unknown>> = {
// [k in keyof T as string extends k
// ? never
// : k extends string
// ? T[k] extends OptionalOutSchema
// ? never
// : k
// : never]: T[k]["_zod"]["output"];
// } & {
// [k in keyof T as string extends k
// ? never
// : k extends string
// ? T[k] extends OptionalOutSchema
// ? k
// : never
// : never]?: T[k]["_zod"]["output"];
// } & Extra;
export type $InferObjectInput<T extends $ZodLooseShape, Extra extends Record<string, unknown>> = string extends keyof T
? util.IsAny<T[keyof T]> extends true
? Record<string, unknown>
@@ -1596,33 +1637,16 @@ export type $InferObjectInput<T extends $ZodLooseShape, Extra extends Record<str
} & Extra
>;
function handleObjectResult(result: ParsePayload, final: ParsePayload, key: PropertyKey) {
// if(isOptional)
function handlePropertyResult(result: ParsePayload, final: ParsePayload, key: PropertyKey, input: any) {
if (result.issues.length) {
final.issues.push(...util.prefixIssues(key, result.issues));
}
(final.value as any)[key] = result.value;
}
function handleOptionalObjectResult(result: ParsePayload, final: ParsePayload, key: PropertyKey, input: any) {
if (result.issues.length) {
// validation failed against value schema
if (input[key] === undefined) {
// if input was undefined, ignore the error
if (key in input) {
(final.value as any)[key] = undefined;
} else {
(final.value as any)[key] = result.value;
}
} else {
final.issues.push(...util.prefixIssues(key, result.issues));
if (result.value === undefined) {
if (key in input) {
(final.value as any)[key] = undefined;
}
} else if (result.value === undefined) {
// validation returned `undefined`
if (key in input) (final.value as any)[key] = undefined;
} else {
// non-undefined value
(final.value as any)[key] = result.value;
}
}
@@ -1732,41 +1756,25 @@ export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$con
// A: preserve key order {
doc.write(`const newResult = {}`);
for (const key of normalized.keys) {
if (normalized.optionalKeys.has(key)) {
const id = ids[key];
doc.write(`const ${id} = ${parseStr(key)};`);
const k = util.esc(key);
doc.write(`
const id = ids[key];
const k = util.esc(key);
doc.write(`const ${id} = ${parseStr(key)};`);
doc.write(`
if (${id}.issues.length) {
if (input[${k}] === undefined) {
if (${k} in input) {
newResult[${k}] = undefined;
}
} else {
payload.issues = payload.issues.concat(
${id}.issues.map((iss) => ({
...iss,
path: iss.path ? [${k}, ...iss.path] : [${k}],
}))
);
payload.issues = payload.issues.concat(${id}.issues.map(iss => ({
...iss,
path: iss.path ? [${k}, ...iss.path] : [${k}]
})));
}
if (${id}.value === undefined) {
if (${k} in input) {
newResult[${k}] = undefined;
}
} else if (${id}.value === undefined) {
if (${k} in input) newResult[${k}] = undefined;
} else {
newResult[${k}] = ${id}.value;
}
`);
} else {
const id = ids[key];
// const id = ids[key];
doc.write(`const ${id} = ${parseStr(key)};`);
doc.write(`
if (${id}.issues.length) payload.issues = payload.issues.concat(${id}.issues.map(iss => ({
...iss,
path: iss.path ? [${util.esc(key)}, ...iss.path] : [${util.esc(key)}]
})));`);
doc.write(`newResult[${util.esc(key)}] = ${id}.value`);
}
`);
}
doc.write(`payload.value = newResult;`);
@@ -1811,39 +1819,16 @@ export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$con
const shape = value.shape;
for (const key of value.keys) {
const el = shape[key]!;
// do not add omitted optional keys
// if (!(key in input)) {
// if (optionalKeys.has(key)) continue;
// payload.issues.push({
// code: "invalid_type",
// path: [key],
// expected: "nonoptional",
// note: `Missing required key: "${key}"`,
// input,
// inst,
// });
// }
const r = el._zod.run({ value: input[key], issues: [] }, ctx);
const isOptional = el._zod.optin === "optional" && el._zod.optout === "optional";
if (r instanceof Promise) {
proms.push(
r.then((r) =>
isOptional ? handleOptionalObjectResult(r, payload, key, input) : handleObjectResult(r, payload, key)
)
);
} else if (isOptional) {
handleOptionalObjectResult(r, payload, key, input);
proms.push(r.then((r) => handlePropertyResult(r, payload, key, input)));
} else {
handleObjectResult(r, payload, key);
handlePropertyResult(r, payload, key, input);
}
}
}
if (!catchall) {
// return payload;
return proms.length ? Promise.all(proms).then(() => payload) : payload;
}
const unrecognized: string[] = [];
@@ -1860,9 +1845,9 @@ export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$con
const r = _catchall.run({ value: input[key], issues: [] }, ctx);
if (r instanceof Promise) {
proms.push(r.then((r) => handleObjectResult(r, payload, key)));
proms.push(r.then((r) => handlePropertyResult(r, payload, key, input)));
} else {
handleObjectResult(r, payload, key);
handlePropertyResult(r, payload, key, input);
}
}
@@ -1925,6 +1910,12 @@ function handleUnionResults(results: ParsePayload[], final: ParsePayload, inst:
}
}
const nonaborted = results.filter((r) => !util.aborted(r));
if (nonaborted.length > 0) {
final.value = nonaborted[0].value;
return nonaborted[0];
}
final.issues.push({
code: "invalid_union",
input: final.value,
@@ -2737,8 +2728,8 @@ export const $ZodEnum: core.$constructor<$ZodEnum> = /*@__PURE__*/ core.$constru
$ZodType.init(inst, def);
const values = util.getEnumValues(def.entries);
inst._zod.values = new Set<util.Primitive>(values);
const valuesSet = new Set<util.Primitive>(values);
inst._zod.values = valuesSet;
inst._zod.pattern = new RegExp(
`^(${values
@@ -2749,7 +2740,7 @@ export const $ZodEnum: core.$constructor<$ZodEnum> = /*@__PURE__*/ core.$constru
inst._zod.parse = (payload, _ctx) => {
const input = payload.value;
if (inst._zod.values.has(input)) {
if (valuesSet.has(input)) {
return payload;
}
payload.issues.push({
@@ -2865,10 +2856,12 @@ export const $ZodLiteral: core.$constructor<$ZodLiteral> = /*@__PURE__*/ core.$c
//////////////////////////////////////////
// provide a fallback in case the File interface isn't provided in the environment
type _File = typeof globalThis extends { File: new (...args: any[]) => any }
? InstanceType<typeof globalThis.File>
: {};
interface File extends _File {}
type _File = typeof globalThis extends { File: infer F extends new (...args: any[]) => any } ? InstanceType<F> : {};
/** Do not reference this directly. */
export interface File extends _File {
type: string;
size: number;
}
export interface $ZodFileDef extends $ZodTypeDef {
type: "file";
@@ -2893,6 +2886,7 @@ export const $ZodFile: core.$constructor<$ZodFile> = /*@__PURE__*/ core.$constru
inst._zod.parse = (payload, _ctx) => {
const input = payload.value;
// @ts-ignore
if (input instanceof File) return payload;
payload.issues.push({
expected: "file",
@@ -3329,7 +3323,7 @@ export interface $ZodCatch<T extends SomeType = $ZodType> extends $ZodType {
export const $ZodCatch: core.$constructor<$ZodCatch> = /*@__PURE__*/ core.$constructor("$ZodCatch", (inst, def) => {
$ZodType.init(inst, def);
inst._zod.optin = "optional";
util.defineLazy(inst._zod, "optin", () => def.innerType._zod.optin);
util.defineLazy(inst._zod, "optout", () => def.innerType._zod.optout);
util.defineLazy(inst._zod, "values", () => def.innerType._zod.values);
@@ -3362,6 +3356,7 @@ export const $ZodCatch: core.$constructor<$ZodCatch> = /*@__PURE__*/ core.$const
},
input: payload.value,
});
payload.issues = [];
}
@@ -777,6 +777,7 @@ export class JSONSchemaGenerator {
} else if (this.target === "draft-7") {
result.$schema = "http://json-schema.org/draft-07/schema#";
} else {
// @ts-ignore
console.warn(`Invalid target: ${this.target}`);
}
+170 -110
View File
@@ -283,6 +283,12 @@ export function assignProp<T extends object, K extends PropertyKey>(
});
}
export function cloneDef(schema: schemas.$ZodType): any {
const def: any = {};
Object.defineProperties(def, Object.getOwnPropertyDescriptors(schema._zod.def));
return def;
}
export function getElementAtPath(obj: any, path: (string | number)[] | null | undefined): any {
if (!path) return obj;
return path.reduce((acc, key) => acc?.[key], obj);
@@ -314,15 +320,16 @@ export function esc(str: string): string {
return JSON.stringify(str);
}
export const captureStackTrace: (targetObject: object, constructorOpt?: Function) => void = Error.captureStackTrace
? Error.captureStackTrace
: (..._args) => {};
export const captureStackTrace: (targetObject: object, constructorOpt?: Function) => void = (
"captureStackTrace" in Error ? Error.captureStackTrace : (..._args: any[]) => {}
) as any;
export function isObject(data: any): data is Record<PropertyKey, unknown> {
return typeof data === "object" && data !== null && !Array.isArray(data);
}
export const allowsEval: { value: boolean } = cached(() => {
// @ts-ignore
if (typeof navigator !== "undefined" && navigator?.userAgent?.includes("Cloudflare")) {
return false;
}
@@ -409,6 +416,7 @@ export const getParsedType = (data: any): ParsedTypes => {
if (typeof Date !== "undefined" && data instanceof Date) {
return "date";
}
// @ts-ignore
if (typeof File !== "undefined" && data instanceof File) {
return "file";
}
@@ -535,71 +543,103 @@ export const BIGINT_FORMAT_RANGES: Record<checks.$ZodBigIntFormats, [bigint, big
};
export function pick(schema: schemas.$ZodObject, mask: Record<string, unknown>): any {
const newShape: Writeable<schemas.$ZodShape> = {};
const currDef = schema._zod.def; //.shape;
const currDef = schema._zod.def;
for (const key in mask) {
if (!(key in currDef.shape)) {
throw new Error(`Unrecognized key: "${key}"`);
}
if (!mask[key]) continue;
const def = cloneDef(schema);
Object.defineProperties(def, {
shape: {
get() {
const newShape: Writeable<schemas.$ZodShape> = {};
for (const key in mask) {
if (!(key in currDef.shape)) {
throw new Error(`Unrecognized key: "${key}"`);
}
if (!mask[key]) continue;
newShape[key] = currDef.shape[key]!;
}
// pick key
newShape[key] = currDef.shape[key]!;
}
assignProp(this, "shape", newShape); // self-caching
return newShape;
},
},
checks: {
value: [],
},
});
return clone(schema, {
...schema._zod.def,
shape: newShape,
checks: [],
}) as any;
return clone(schema, def) as any;
}
export function omit(schema: schemas.$ZodObject, mask: object): any {
const newShape: Writeable<schemas.$ZodShape> = { ...schema._zod.def.shape };
const currDef = schema._zod.def; //.shape;
for (const key in mask) {
if (!(key in currDef.shape)) {
throw new Error(`Unrecognized key: "${key}"`);
}
if (!(mask as any)[key]) continue;
const currDef = schema._zod.def;
delete newShape[key];
}
return clone(schema, {
...schema._zod.def,
shape: newShape,
checks: [],
const def = cloneDef(schema);
Object.defineProperties(def, {
shape: {
get() {
const newShape: Writeable<schemas.$ZodShape> = { ...schema._zod.def.shape };
for (const key in mask) {
if (!(key in currDef.shape)) {
throw new Error(`Unrecognized key: "${key}"`);
}
if (!(mask as any)[key]) continue;
delete newShape[key];
}
assignProp(this, "shape", newShape); // self-caching
return newShape;
},
},
checks: {
value: [],
},
});
return clone(schema, def);
}
export function extend(schema: schemas.$ZodObject, shape: schemas.$ZodShape): any {
if (!isPlainObject(shape)) {
throw new Error("Invalid input to extend: expected a plain object");
}
const def = {
...schema._zod.def,
get shape() {
const _shape = { ...schema._zod.def.shape, ...shape };
assignProp(this, "shape", _shape); // self-caching
return _shape;
const def = cloneDef(schema);
Object.defineProperties(def, {
shape: {
get() {
const _shape = { ...schema._zod.def.shape, ...shape };
assignProp(this, "shape", _shape); // self-caching
return _shape;
},
},
checks: [], // delete existing checks
} as any;
checks: {
value: [],
},
});
return clone(schema, def) as any;
}
export function merge(a: schemas.$ZodObject, b: schemas.$ZodObject): any {
return clone(a, {
...a._zod.def,
get shape() {
const _shape = { ...a._zod.def.shape, ...b._zod.def.shape };
assignProp(this, "shape", _shape); // self-caching
return _shape;
const def = cloneDef(a);
Object.defineProperties(def, {
shape: {
get() {
const _shape = { ...a._zod.def.shape, ...b._zod.def.shape };
assignProp(this, "shape", _shape); // self-caching
return _shape;
},
},
catchall: b._zod.def.catchall,
checks: [], // delete existing checks
}) as any;
catchall: {
get() {
return b._zod.def.catchall;
},
},
checks: {
value: [], // delete existing checks
},
});
return clone(a, def) as any;
}
export function partial(
@@ -607,40 +647,49 @@ export function partial(
schema: schemas.$ZodObject,
mask: object | undefined
): any {
const oldShape = schema._zod.def.shape;
const shape: Writeable<schemas.$ZodShape> = { ...oldShape };
const def = cloneDef(schema);
Object.defineProperties(def, {
shape: {
get() {
const oldShape = schema._zod.def.shape;
const shape: Writeable<schemas.$ZodShape> = { ...oldShape };
if (mask) {
for (const key in mask) {
if (!(key in oldShape)) {
throw new Error(`Unrecognized key: "${key}"`);
}
if (!(mask as any)[key]) continue;
// if (oldShape[key]!._zod.optin === "optional") continue;
shape[key] = Class
? new Class({
type: "optional",
innerType: oldShape[key]!,
})
: oldShape[key]!;
}
} else {
for (const key in oldShape) {
// if (oldShape[key]!._zod.optin === "optional") continue;
shape[key] = Class
? new Class({
type: "optional",
innerType: oldShape[key]!,
})
: oldShape[key]!;
}
}
if (mask) {
for (const key in mask) {
if (!(key in oldShape)) {
throw new Error(`Unrecognized key: "${key}"`);
}
if (!(mask as any)[key]) continue;
// if (oldShape[key]!._zod.optin === "optional") continue;
shape[key] = Class
? new Class({
type: "optional",
innerType: oldShape[key]!,
})
: oldShape[key]!;
}
} else {
for (const key in oldShape) {
// if (oldShape[key]!._zod.optin === "optional") continue;
shape[key] = Class
? new Class({
type: "optional",
innerType: oldShape[key]!,
})
: oldShape[key]!;
}
}
return clone(schema, {
...schema._zod.def,
shape,
checks: [],
}) as any;
assignProp(this, "shape", shape); // self-caching
return shape;
},
},
checks: {
value: [],
},
});
return clone(schema, def) as any;
}
export function required(
@@ -648,44 +697,54 @@ export function required(
schema: schemas.$ZodObject,
mask: object | undefined
): any {
const oldShape = schema._zod.def.shape;
const shape: Writeable<schemas.$ZodShape> = { ...oldShape };
const def = cloneDef(schema);
Object.defineProperties(def, {
shape: {
get() {
const oldShape = schema._zod.def.shape;
const shape: Writeable<schemas.$ZodShape> = { ...oldShape };
if (mask) {
for (const key in mask) {
if (!(key in shape)) {
throw new Error(`Unrecognized key: "${key}"`);
}
if (!(mask as any)[key]) continue;
// overwrite with non-optional
shape[key] = new Class({
type: "nonoptional",
innerType: oldShape[key]!,
});
}
} else {
for (const key in oldShape) {
// overwrite with non-optional
shape[key] = new Class({
type: "nonoptional",
innerType: oldShape[key]!,
});
}
}
if (mask) {
for (const key in mask) {
if (!(key in shape)) {
throw new Error(`Unrecognized key: "${key}"`);
}
if (!(mask as any)[key]) continue;
// overwrite with non-optional
shape[key] = new Class({
type: "nonoptional",
innerType: oldShape[key]!,
});
}
} else {
for (const key in oldShape) {
// overwrite with non-optional
shape[key] = new Class({
type: "nonoptional",
innerType: oldShape[key]!,
});
}
}
return clone(schema, {
...schema._zod.def,
shape,
// optional: [],
checks: [],
}) as any;
assignProp(this, "shape", shape); // self-caching
return shape;
},
},
checks: {
value: [],
},
});
return clone(schema, def) as any;
}
export type Constructor<T, Def extends any[] = any[]> = new (...args: Def) => T;
export function aborted(x: schemas.ParsePayload, startIndex = 0): boolean {
for (let i = startIndex; i < x.issues.length; i++) {
if (x.issues[i]?.continue !== true) return true;
if (x.issues[i]?.continue !== true) {
return true;
}
}
return false;
}
@@ -733,6 +792,7 @@ export function finalizeIssue(
export function getSizableOrigin(input: any): "set" | "map" | "file" | "unknown" {
if (input instanceof Set) return "set";
if (input instanceof Map) return "map";
// @ts-ignore
if (input instanceof File) return "file";
return "unknown";
}
+1 -1
View File
@@ -1,5 +1,5 @@
export const version = {
major: 4,
minor: 0,
patch: 5 as number,
patch: 6 as number,
} as const;
+3 -1
View File
@@ -1024,9 +1024,11 @@ export function partialRecord<Key extends core.$ZodRecordKey, Value extends Some
valueType: Value,
params?: string | core.$ZodRecordParams
): ZodMiniRecord<Key & core.$partial, Value> {
const k = core.clone(keyType);
k._zod.values = undefined;
return new ZodMiniRecord({
type: "record",
keyType: union([keyType, never()]),
keyType: k,
valueType: valueType as any,
...util.normalizeParams(params),
}) as any;
+3 -1
View File
@@ -3,6 +3,8 @@
"compilerOptions": {
"module": "nodenext",
"customConditions": ["@zod/source"],
"noEmit": true,
"types": ["vitest", "recheck"]
},
"include": ["**/*.ts"]
"include": ["**/*.ts"],
}
+2 -18
View File
@@ -1,19 +1,3 @@
import { z } from "zod";
import * as z from "zod";
z;
const arg = z.discriminatedUnion("type", [
z.object({
type: z.literal("play"),
id: z.string(),
name: z.string(),
duration: z.number().int().positive(),
}),
z.object({
type: z.literal("pause"),
id: z.string(),
reason: z.string().optional(),
}),
]);
arg.def;
console.log(z.url().parse("sup")); // ❌
+14 -42
View File
@@ -17,9 +17,6 @@ importers:
'@types/benchmark':
specifier: ^2.1.5
version: 2.1.5
'@types/node':
specifier: ^20.17.30
version: 20.17.30
'@types/semver':
specifier: ^7.7.0
version: 7.7.0
@@ -97,7 +94,7 @@ importers:
version: 5.5.4
vitest:
specifier: ^2.1.9
version: 2.1.9(@types/node@20.17.30)([email protected])([email protected])
version: 2.1.9(@types/node@22.13.13)([email protected])([email protected])
zod:
specifier: workspace:*
version: link:packages/zod
@@ -2590,9 +2587,6 @@ packages:
'@types/[email protected]':
resolution: {integrity: sha512-hHTHp+sEz6SxFsp+SA+Tqrua3AbmlAw+Y//aEwdHrdZkYVRWdvWD3y5uPZ0flYOkgskaFWqZ/YGFm3FaFQ0pRw==}
'@types/[email protected]':
resolution: {integrity: sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==}
'@types/[email protected]':
resolution: {integrity: sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==}
@@ -3123,15 +3117,6 @@ packages:
[email protected]:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
[email protected]:
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
[email protected]:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
@@ -5374,9 +5359,6 @@ packages:
[email protected]:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
[email protected]:
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
[email protected]:
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
@@ -7686,10 +7668,6 @@ snapshots:
dependencies:
undici-types: 5.26.5
'@types/[email protected]':
dependencies:
undici-types: 6.19.8
'@types/[email protected]':
dependencies:
undici-types: 6.20.0
@@ -7753,13 +7731,13 @@ snapshots:
chai: 5.2.0
tinyrainbow: 1.2.0
'@vitest/[email protected]([email protected](@types/node@20.17.30)([email protected])([email protected]))':
'@vitest/[email protected]([email protected](@types/node@22.13.13)([email protected])([email protected]))':
dependencies:
'@vitest/spy': 2.1.9
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
vite: 5.4.18(@types/node@20.17.30)([email protected])([email protected])
vite: 5.4.18(@types/node@22.13.13)([email protected])([email protected])
'@vitest/[email protected]':
dependencies:
@@ -8221,10 +8199,6 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
ms: 2.1.3
[email protected]:
dependencies:
ms: 2.1.3
@@ -11091,8 +11065,6 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
@@ -11231,13 +11203,13 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.2
[email protected](@types/node@20.17.30)([email protected])([email protected]):
[email protected](@types/node@22.13.13)([email protected])([email protected]):
dependencies:
cac: 6.7.14
debug: 4.4.0
debug: 4.4.1
es-module-lexer: 1.6.0
pathe: 1.1.2
vite: 5.4.18(@types/node@20.17.30)([email protected])([email protected])
vite: 5.4.18(@types/node@22.13.13)([email protected])([email protected])
transitivePeerDependencies:
- '@types/node'
- less
@@ -11249,28 +11221,28 @@ snapshots:
- supports-color
- terser
[email protected](@types/node@20.17.30)([email protected])([email protected]):
[email protected](@types/node@22.13.13)([email protected])([email protected]):
dependencies:
esbuild: 0.21.5
postcss: 8.5.3
rollup: 4.39.0
optionalDependencies:
'@types/node': 20.17.30
'@types/node': 22.13.13
fsevents: 2.3.3
lightningcss: 1.29.3
terser: 5.39.0
[email protected](@types/node@20.17.30)([email protected])([email protected]):
[email protected](@types/node@22.13.13)([email protected])([email protected]):
dependencies:
'@vitest/expect': 2.1.9
'@vitest/mocker': 2.1.9([email protected](@types/node@20.17.30)([email protected])([email protected]))
'@vitest/mocker': 2.1.9([email protected](@types/node@22.13.13)([email protected])([email protected]))
'@vitest/pretty-format': 2.1.9
'@vitest/runner': 2.1.9
'@vitest/snapshot': 2.1.9
'@vitest/spy': 2.1.9
'@vitest/utils': 2.1.9
chai: 5.2.0
debug: 4.4.0
debug: 4.4.1
expect-type: 1.2.1
magic-string: 0.30.17
pathe: 1.1.2
@@ -11279,11 +11251,11 @@ snapshots:
tinyexec: 0.3.2
tinypool: 1.0.2
tinyrainbow: 1.2.0
vite: 5.4.18(@types/node@20.17.30)([email protected])([email protected])
vite-node: 2.1.9(@types/node@20.17.30)([email protected])([email protected])
vite: 5.4.18(@types/node@22.13.13)([email protected])([email protected])
vite-node: 2.1.9(@types/node@22.13.13)([email protected])([email protected])
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 20.17.30
'@types/node': 22.13.13
transitivePeerDependencies:
- less
- lightningcss
+3 -1
View File
@@ -1,9 +1,11 @@
{
"extends": "./.configs/tsconfig.base.json",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"customConditions": ["@zod/source"],
"module": "nodenext",
"moduleResolution": "nodenext"
"moduleResolution": "nodenext",
"types": ["node"]
},
"include": ["**/*.ts", "**/*.mts", "**/*.cts"],
"exclude": ["**/*.d.ts", "**/*.d.cts", "**/*.d.mts", "packages/docs"]