From 086273160e5952ee661a7b9db82fc757f0e68eab Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Fri, 1 Nov 2024 14:06:47 -0700 Subject: [PATCH 01/30] Add LibLab as Platinum sponsor (#3828) * Tweak * Tweak * Tweak * Tweak * Tweak * Tweak --- README.md | 18 ++++++++++++++++++ deno/lib/README.md | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/README.md b/README.md index cdae9403c..b1246310b 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,24 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod

Platinum

+ + +
+

+

+ + + + LibLab + + +
+ Your API Deserves Better SDKs +
+ liblab.com +

+

+

diff --git a/deno/lib/README.md b/deno/lib/README.md index a721693b1..b1246310b 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -212,6 +212,24 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod

Platinum

+ + + diff --git a/deno/lib/README.md b/deno/lib/README.md index b1246310b..a3c888695 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -216,16 +216,16 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod From f487d74ecd3ae703ef8932462d14d643e31658b3 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Wed, 6 Nov 2024 19:35:12 -0800 Subject: [PATCH 03/30] Remove faulty ip test case --- deno/lib/__tests__/string.test.ts | 1 - deno/lib/types.ts | 2 ++ src/__tests__/string.test.ts | 1 - src/types.ts | 2 ++ 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index 564fa84d6..64438717a 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -740,7 +740,6 @@ test("IP validation", () => { const validIPs = [ "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c", "9d4:c956:420f:5788:4339:9b3b:2418:75c3", - "a6ea::2454:a5ce:94.105.123.75", "474f:4c83::4e40:a47:ff95:0cda", "d329:0:25b4:db47:a9d1:0:4926:0000", "e48:10fb:1499:3e28:e4b6:dea5:4692:912c", diff --git a/deno/lib/types.ts b/deno/lib/types.ts index bb2f08519..5d020d278 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -609,6 +609,8 @@ let emojiRegex: RegExp; const ipv4Regex = /^(?:(?: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])$/; +// const ipv6Regex = +// /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,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}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index 368dace7f..f7037fcc2 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -739,7 +739,6 @@ test("IP validation", () => { const validIPs = [ "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c", "9d4:c956:420f:5788:4339:9b3b:2418:75c3", - "a6ea::2454:a5ce:94.105.123.75", "474f:4c83::4e40:a47:ff95:0cda", "d329:0:25b4:db47:a9d1:0:4926:0000", "e48:10fb:1499:3e28:e4b6:dea5:4692:912c", diff --git a/src/types.ts b/src/types.ts index 5aa30b900..f3730ae14 100644 --- a/src/types.ts +++ b/src/types.ts @@ -609,6 +609,8 @@ let emojiRegex: RegExp; const ipv4Regex = /^(?:(?: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])$/; +// const ipv6Regex = +// /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,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}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; From 06182dcfa2c4c00305d9999e815e1675834fc695 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Tue, 12 Nov 2024 14:03:19 -0800 Subject: [PATCH 04/30] Add FUNDING.json --- FUNDING.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 FUNDING.json diff --git a/FUNDING.json b/FUNDING.json new file mode 100644 index 000000000..4cb16898d --- /dev/null +++ b/FUNDING.json @@ -0,0 +1,7 @@ +{ + "drips": { + "ethereum": { + "ownedBy": "0xAe9ae688557471b0317734a54bE095b3C675DA2f" + } + } +} From 207205cab4b4f33f9274a9cce5040d6ced692300 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Tue, 12 Nov 2024 16:10:00 -0800 Subject: [PATCH 05/30] Add tea.yaml --- tea.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tea.yaml diff --git a/tea.yaml b/tea.yaml new file mode 100644 index 000000000..840c05de9 --- /dev/null +++ b/tea.yaml @@ -0,0 +1,6 @@ +# https://tea.xyz/what-is-this-file +--- +version: 1.0.0 +codeOwners: + - '0xF233A42130Bcdd8b22FFB5D9593199f31C3Eeb87' +quorum: 1 From 48f1c4793b21b19714d68f970ae3d739263e2b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Tue, 10 Dec 2024 02:25:03 +0100 Subject: [PATCH 06/30] docs: Remove invalid semicolon in ERROR_HANDLING.md (#3857) --- ERROR_HANDLING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERROR_HANDLING.md b/ERROR_HANDLING.md index 72d95ec1b..78dbb29ff 100644 --- a/ERROR_HANDLING.md +++ b/ERROR_HANDLING.md @@ -95,7 +95,7 @@ const person = z.object({ address: z.object({ line1: z.string(), zipCode: z.number().min(10000), // American 5-digit code - }).strict(); // do not allow unrecognized keys + }).strict() // do not allow unrecognized keys }); ``` From fba6d02bb5bb43cb69cd86768af5938b3c5fca7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=20Bracanovi=C4=87?= Date: Mon, 9 Dec 2024 17:25:20 -0800 Subject: [PATCH 07/30] Update README.md (#3851) Add antd-zod form integration --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a3c888695..73ef668ec 100644 --- a/README.md +++ b/README.md @@ -510,6 +510,7 @@ There are a growing number of tools that are built atop or support Zod natively! - [`mobx-zod-form`](https://github.com/MonoidDev/mobx-zod-form): Data-first form builder based on MobX & Zod. - [`@vee-validate/zod`](https://github.com/logaretm/vee-validate/tree/main/packages/zod): Form library for Vue.js with Zod schema validation. - [`zod-form-renderer`](https://github.com/thepeaklab/zod-form-renderer): Auto-infer form fields from zod schema and render them with react-hook-form with E2E type safety. +- [`antd-zod`](https://github.com/MrBr/antd-zod): Zod adapter for Ant Design form fields validation. #### Zod to X From 1d0a4b95300a2c470b175ed4524fe3cf04ef9b19 Mon Sep 17 00:00:00 2001 From: Christoffer Date: Tue, 10 Dec 2024 02:26:39 +0100 Subject: [PATCH 08/30] fix: bigint coerce crash (#3822) * fix: cast invalid bigint inputs to undefined * fix: cast invalid bigint values to undefined * refactor: return invalid type on coerce error --- deno/lib/types.ts | 24 ++++++++++++++++-------- src/types.ts | 24 ++++++++++++++++-------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 5d020d278..9cee39a35 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -1551,17 +1551,15 @@ export interface ZodBigIntDef extends ZodTypeDef { export class ZodBigInt extends ZodType { _parse(input: ParseInput): ParseReturnType { if (this._def.coerce) { - input.data = BigInt(input.data); + try { + input.data = BigInt(input.data); + } catch { + return this._getInvalidInput(input); + } } const parsedType = this._getType(input); if (parsedType !== ZodParsedType.bigint) { - const ctx = this._getOrReturnCtx(input); - addIssueToContext(ctx, { - code: ZodIssueCode.invalid_type, - expected: ZodParsedType.bigint, - received: ctx.parsedType, - }); - return INVALID; + return this._getInvalidInput(input); } let ctx: undefined | ParseContext = undefined; @@ -1616,6 +1614,16 @@ export class ZodBigInt extends ZodType { return { status: status.value, value: input.data }; } + _getInvalidInput(input: ParseInput) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.bigint, + received: ctx.parsedType, + }); + return INVALID; + } + static create = ( params?: RawCreateParams & { coerce?: boolean } ): ZodBigInt => { diff --git a/src/types.ts b/src/types.ts index f3730ae14..be49c7012 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1551,17 +1551,15 @@ export interface ZodBigIntDef extends ZodTypeDef { export class ZodBigInt extends ZodType { _parse(input: ParseInput): ParseReturnType { if (this._def.coerce) { - input.data = BigInt(input.data); + try { + input.data = BigInt(input.data); + } catch { + return this._getInvalidInput(input); + } } const parsedType = this._getType(input); if (parsedType !== ZodParsedType.bigint) { - const ctx = this._getOrReturnCtx(input); - addIssueToContext(ctx, { - code: ZodIssueCode.invalid_type, - expected: ZodParsedType.bigint, - received: ctx.parsedType, - }); - return INVALID; + return this._getInvalidInput(input); } let ctx: undefined | ParseContext = undefined; @@ -1616,6 +1614,16 @@ export class ZodBigInt extends ZodType { return { status: status.value, value: input.data }; } + _getInvalidInput(input: ParseInput) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.bigint, + received: ctx.parsedType, + }); + return INVALID; + } + static create = ( params?: RawCreateParams & { coerce?: boolean } ): ZodBigInt => { From dfe9b7c4ebb4ee1471e84b63e52524c0c4318525 Mon Sep 17 00:00:00 2001 From: Derek Johnson Date: Mon, 9 Dec 2024 20:28:53 -0500 Subject: [PATCH 09/30] Update README.md (#3829) Fixed link for documentation on Date objects to not point to string dates --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 73ef668ec..8f9c37c44 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ - [BigInts](#bigints) - [NaNs](#nans) - [Booleans](#booleans) -- [Dates](#dates) +- [Dates](#dates-1) - [Zod enums](#zod-enums) - [Native enums](#native-enums) - [Optionals](#optionals) From 14dceaa2d2b27ef448b48c4f0641413e3ead974d Mon Sep 17 00:00:00 2001 From: Marcel Fischer Date: Tue, 10 Dec 2024 02:29:08 +0100 Subject: [PATCH 10/30] Add API library (#3814) Add oas-tszod-gen to API libraries section. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8f9c37c44..c74a91ac4 100644 --- a/README.md +++ b/README.md @@ -492,6 +492,7 @@ There are a growing number of tools that are built atop or support Zod natively! - [`tapiduck`](https://github.com/sumukhbarve/monoduck/blob/main/src/tapiduck/README.md): End-to-end typesafe JSON APIs with Zod and Express; a bit like tRPC, but simpler. - [`koa-zod-router`](https://github.com/JakeFenley/koa-zod-router): Create typesafe routes in Koa with I/O validation using Zod. - [`zod-sockets`](https://github.com/RobinTail/zod-sockets): Zod-powered Socket.IO microframework with I/O validation and built-in AsyncAPI specs +- [`oas-tszod-gen`](https://github.com/inkognitro/oas-tszod-gen): Client SDK code generator to convert OpenApi v3 specifications into TS endpoint caller functions with Zod types. #### Form integrations From d50976a4163f54ef4d7de3c3c51f7236dcab5ce1 Mon Sep 17 00:00:00 2001 From: Phil Hawksworth Date: Tue, 10 Dec 2024 01:30:20 +0000 Subject: [PATCH 11/30] Simplifying installation guide now that Deno supports installing from npm (#3859) * Simplify install guide Since Deno now supports installing directly from npm, we can streamline the install guide * Update deno install info in translation --- README.md | 20 ++++---------------- README_ZH.md | 16 ++-------------- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index c74a91ac4..3cf25ab2d 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,7 @@ - [Utilities for Zod](#utilities-for-zod) - [Installation](#installation) - [Requirements](#requirements) - - [From `npm` (Node/Bun)](#from-npm-nodebun) - - [From `deno.land/x` (Deno)](#from-denolandx-deno) + - [From `npm`](#from-npm) - [Basic usage](#basic-usage) - [Primitives](#primitives) - [Coercion for primitives](#coercion-for-primitives) @@ -594,10 +593,11 @@ There are a growing number of tools that are built atop or support Zod natively! } ``` -### From `npm` (Node/Bun) +### From `npm` ```sh npm install zod # npm +deno add npm:zod # deno yarn add zod # yarn bun add zod # bun pnpm add zod # pnpm @@ -607,24 +607,12 @@ Zod also publishes a canary version on every commit. To install the canary: ```sh npm install zod@canary # npm +deno add npm:zod@canary # deno yarn add zod@canary # yarn bun add zod@canary # bun pnpm add zod@canary # pnpm ``` -### From `deno.land/x` (Deno) - -Unlike Node, Deno relies on direct URL imports instead of a package manager like NPM. Zod is available on [deno.land/x](https://deno.land/x). The latest version can be imported like so: - -```ts -import { z } from "https://deno.land/x/zod/mod.ts"; -``` - -You can also specify a particular version: - -```ts -import { z } from "https://deno.land/x/zod@v3.16.1/mod.ts"; -``` > The rest of this README assumes you are using npm and importing directly from the `"zod"` package. diff --git a/README_ZH.md b/README_ZH.md index 0c9ab46a0..09bf6b111 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -316,28 +316,16 @@ _要在这里看到你的名字 + Twitter + 網站 , 请在[Freelancer](https:// } ``` -### 从`npm`(Node/Bun)安装 +### 从`npm` 安装 ```sh npm install zod +deno add npm:zod # deno yarn add zod # yarn bun add zod # bun pnpm add zod # pnpm ``` -### 从`deno.land/x` (Deno)安装 - -和 Node 不同,Deno 依靠一个直接的 URL 导入而非像 npm 这样的包管理器。可以这样导入最新版本的 Zod: - -```ts -import { z } from "https://deno.land/x/zod/mod.ts"; -``` - -你也可以指定一个具体的版本: - -```ts -import { z } from "https://deno.land/x/zod@v3.16.1/mod.ts"; -``` > README 的剩余部分假定你是直接通过 npm 安装的`zod`包。 From f82f817252c1f1342d81a2a5ae9adf426cb32cec Mon Sep 17 00:00:00 2001 From: Ryo Watanabe Date: Tue, 10 Dec 2024 10:38:09 +0900 Subject: [PATCH 12/30] feat: z.string.cidr() - support CIDR notation (#3820) * feat: support cidr * docs * feat: z.string().cidr() * fix * Simplify --------- Co-authored-by: Colin McDonnell --- README.md | 25 ++++++++++++- deno/lib/README.md | 25 ++++++++++++- deno/lib/ZodError.ts | 1 + deno/lib/__tests__/string.test.ts | 60 +++++++++++++++++++++++++++++++ deno/lib/types.ts | 33 +++++++++++++++++ src/ZodError.ts | 1 + src/__tests__/string.test.ts | 60 +++++++++++++++++++++++++++++++ src/types.ts | 33 +++++++++++++++++ 8 files changed, 236 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3cf25ab2d..c60c133fd 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ - [Dates](#dates) - [Times](#times) - [IP addresses](#ip-addresses) + - [IP ranges](#ip-ranges-cidr) - [Numbers](#numbers) - [BigInts](#bigints) - [NaNs](#nans) @@ -777,6 +778,7 @@ z.string().startsWith(string); z.string().endsWith(string); z.string().datetime(); // ISO 8601; by default only `Z` timezone allowed z.string().ip(); // defaults to allow both IPv4 and IPv6 +z.string().cidr(); // defaults to allow both IPv4 and IPv6 // transforms z.string().trim(); // trim whitespace @@ -818,6 +820,7 @@ z.string().datetime({ message: "Invalid datetime string! Must be UTC." }); z.string().date({ message: "Invalid date string!" }); z.string().time({ message: "Invalid time string!" }); z.string().ip({ message: "Invalid IP address" }); +z.string().cidr({ message: "Invalid CIDR" }); ``` ### Datetimes @@ -900,7 +903,7 @@ time.parse("00:00:00"); // fail ### IP addresses -The `z.string().ip()` method by default validate IPv4 and IPv6. +By default `.ip()` allows both IPv4 and IPv6. ```ts const ip = z.string().ip(); @@ -923,6 +926,26 @@ const ipv6 = z.string().ip({ version: "v6" }); ipv6.parse("192.168.1.1"); // fail ``` +### IP ranges (CIDR) + +Validate IP address ranges specified with [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing). By default, `.cidr()` allows both IPv4 and IPv6. + +```ts +const cidr = z.string().cidr(); +cidr.parse("192.168.0.0/24"); // pass +cidr.parse("2001:db8::/32"); // pass +``` + +You can specify a version with the `version` parameter. + +```ts +const ipv4Cidr = z.string().cidr({ version: "v4" }); +ipv4Cidr.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003"); // fail + +const ipv6Cidr = z.string().cidr({ version: "v6" }); +ipv6Cidr.parse("192.168.1.1"); // fail +``` + ## Numbers You can customize certain error messages when creating a number schema. diff --git a/deno/lib/README.md b/deno/lib/README.md index a3c888695..2f5af57c2 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -76,6 +76,7 @@ - [Dates](#dates) - [Times](#times) - [IP addresses](#ip-addresses) + - [IP ranges](#ip-ranges-cidr) - [Numbers](#numbers) - [BigInts](#bigints) - [NaNs](#nans) @@ -787,6 +788,7 @@ z.string().startsWith(string); z.string().endsWith(string); z.string().datetime(); // ISO 8601; by default only `Z` timezone allowed z.string().ip(); // defaults to allow both IPv4 and IPv6 +z.string().cidr(); // defaults to allow both IPv4 and IPv6 // transforms z.string().trim(); // trim whitespace @@ -828,6 +830,7 @@ z.string().datetime({ message: "Invalid datetime string! Must be UTC." }); z.string().date({ message: "Invalid date string!" }); z.string().time({ message: "Invalid time string!" }); z.string().ip({ message: "Invalid IP address" }); +z.string().cidr({ message: "Invalid CIDR" }); ``` ### Datetimes @@ -910,7 +913,7 @@ time.parse("00:00:00"); // fail ### IP addresses -The `z.string().ip()` method by default validate IPv4 and IPv6. +By default `.ip()` allows both IPv4 and IPv6. ```ts const ip = z.string().ip(); @@ -933,6 +936,26 @@ const ipv6 = z.string().ip({ version: "v6" }); ipv6.parse("192.168.1.1"); // fail ``` +### IP ranges (CIDR) + +Validate IP address ranges specified with [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing). By default, `.cidr()` allows both IPv4 and IPv6. + +```ts +const cidr = z.string().cidr(); +cidr.parse("192.168.0.0/24"); // pass +cidr.parse("2001:db8::/32"); // pass +``` + +You can specify a version with the `version` parameter. + +```ts +const ipv4Cidr = z.string().cidr({ version: "v4" }); +ipv4Cidr.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003"); // fail + +const ipv6Cidr = z.string().cidr({ version: "v6" }); +ipv6Cidr.parse("192.168.1.1"); // fail +``` + ## Numbers You can customize certain error messages when creating a number schema. diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts index e757cd8ba..21ad657a3 100644 --- a/deno/lib/ZodError.ts +++ b/deno/lib/ZodError.ts @@ -103,6 +103,7 @@ export type StringValidation = | "time" | "duration" | "ip" + | "cidr" | "base64" | { includes: string; position?: number } | { startsWith: string } diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index 64438717a..3c059f0b0 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -373,6 +373,7 @@ test("checks getters", () => { expect(z.string().email().isUUID).toEqual(false); expect(z.string().email().isNANOID).toEqual(false); expect(z.string().email().isIP).toEqual(false); + expect(z.string().email().isCIDR).toEqual(false); expect(z.string().email().isULID).toEqual(false); expect(z.string().url().isEmail).toEqual(false); @@ -382,6 +383,7 @@ test("checks getters", () => { expect(z.string().url().isUUID).toEqual(false); expect(z.string().url().isNANOID).toEqual(false); expect(z.string().url().isIP).toEqual(false); + expect(z.string().url().isCIDR).toEqual(false); expect(z.string().url().isULID).toEqual(false); expect(z.string().cuid().isEmail).toEqual(false); @@ -391,6 +393,7 @@ test("checks getters", () => { expect(z.string().cuid().isUUID).toEqual(false); expect(z.string().cuid().isNANOID).toEqual(false); expect(z.string().cuid().isIP).toEqual(false); + expect(z.string().cuid().isCIDR).toEqual(false); expect(z.string().cuid().isULID).toEqual(false); expect(z.string().cuid2().isEmail).toEqual(false); @@ -400,6 +403,7 @@ test("checks getters", () => { expect(z.string().cuid2().isUUID).toEqual(false); expect(z.string().cuid2().isNANOID).toEqual(false); expect(z.string().cuid2().isIP).toEqual(false); + expect(z.string().cuid2().isCIDR).toEqual(false); expect(z.string().cuid2().isULID).toEqual(false); expect(z.string().uuid().isEmail).toEqual(false); @@ -409,6 +413,7 @@ test("checks getters", () => { expect(z.string().uuid().isUUID).toEqual(true); expect(z.string().uuid().isNANOID).toEqual(false); expect(z.string().uuid().isIP).toEqual(false); + expect(z.string().uuid().isCIDR).toEqual(false); expect(z.string().uuid().isULID).toEqual(false); expect(z.string().nanoid().isEmail).toEqual(false); @@ -418,6 +423,7 @@ test("checks getters", () => { expect(z.string().nanoid().isUUID).toEqual(false); expect(z.string().nanoid().isNANOID).toEqual(true); expect(z.string().nanoid().isIP).toEqual(false); + expect(z.string().nanoid().isCIDR).toEqual(false); expect(z.string().nanoid().isULID).toEqual(false); expect(z.string().ip().isEmail).toEqual(false); @@ -427,8 +433,19 @@ test("checks getters", () => { expect(z.string().ip().isUUID).toEqual(false); expect(z.string().ip().isNANOID).toEqual(false); expect(z.string().ip().isIP).toEqual(true); + expect(z.string().ip().isCIDR).toEqual(false); expect(z.string().ip().isULID).toEqual(false); + expect(z.string().cidr().isEmail).toEqual(false); + expect(z.string().cidr().isURL).toEqual(false); + expect(z.string().cidr().isCUID).toEqual(false); + expect(z.string().cidr().isCUID2).toEqual(false); + expect(z.string().cidr().isUUID).toEqual(false); + expect(z.string().cidr().isNANOID).toEqual(false); + expect(z.string().cidr().isIP).toEqual(false); + expect(z.string().cidr().isCIDR).toEqual(true); + expect(z.string().cidr().isULID).toEqual(false); + expect(z.string().ulid().isEmail).toEqual(false); expect(z.string().ulid().isURL).toEqual(false); expect(z.string().ulid().isCUID).toEqual(false); @@ -436,6 +453,7 @@ test("checks getters", () => { expect(z.string().ulid().isUUID).toEqual(false); expect(z.string().ulid().isNANOID).toEqual(false); expect(z.string().ulid().isIP).toEqual(false); + expect(z.string().ulid().isCIDR).toEqual(false); expect(z.string().ulid().isULID).toEqual(true); }); @@ -769,3 +787,45 @@ test("IP validation", () => { invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false) ).toBe(true); }); + +test("CIDR validation", () => { + const ipv4Cidr = z.string().cidr({ version: "v4" }); + expect(() => ipv4Cidr.parse("2001:0db8:85a3::8a2e:0370:7334/64")).toThrow(); + + const ipv6Cidr = z.string().cidr({ version: "v6" }); + expect(() => ipv6Cidr.parse("192.168.0.1/24")).toThrow(); + + const validCidrs = [ + "192.168.0.0/24", + "10.0.0.0/8", + "203.0.113.0/24", + "192.0.2.0/24", + "127.0.0.0/8", + "172.16.0.0/12", + "192.168.1.0/24", + "fc00::/7", + "fd00::/8", + "2001:db8::/32", + "2607:f0d0:1002:51::4/64", + "2001:0db8:85a3:0000:0000:8a2e:0370:7334/128", + "2001:0db8:1234:0000::/64", + ]; + + const invalidCidrs = [ + "192.168.1.1/33", + "10.0.0.1/-1", + "192.168.1.1/24/24", + "192.168.1.0/abc", + "2001:db8::1/129", + "2001:db8::1/-1", + "2001:db8::1/64/64", + "2001:db8::1/abc", + ]; + + // no parameters check IPv4 or IPv6 + const cidrSchema = z.string().cidr(); + expect(validCidrs.every((ip) => cidrSchema.safeParse(ip).success)).toBe(true); + expect( + invalidCidrs.every((ip) => cidrSchema.safeParse(ip).success === false) + ).toBe(true); +}); diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 9cee39a35..42d2606bb 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -565,6 +565,7 @@ export type ZodStringCheck = } | { kind: "duration"; message?: string } | { kind: "ip"; version?: IpVersion; message?: string } + | { kind: "cidr"; version?: IpVersion; message?: string } | { kind: "base64"; message?: string }; export interface ZodStringDef extends ZodTypeDef { @@ -608,11 +609,15 @@ let emojiRegex: RegExp; // faster, simpler, safer const ipv4Regex = /^(?:(?: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])$/; +const ipv4CidrRegex = + /^(?:(?: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])\/(3[0-2]|[12]?[0-9])$/; // const ipv6Regex = // /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,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}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +const ipv6CidrRegex = + /^(([0-9a-fA-F]{1,4}:){7,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}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; // https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript const base64Regex = @@ -671,6 +676,17 @@ function isValidIP(ip: string, version?: IpVersion) { return false; } +function isValidCidr(ip: string, version?: IpVersion) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + + return false; +} + export class ZodString extends ZodType { _parse(input: ParseInput): ParseReturnType { if (this._def.coerce) { @@ -933,6 +949,16 @@ export class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else if (check.kind === "base64") { if (!base64Regex.test(input.data)) { ctx = this._getOrReturnCtx(input, ctx); @@ -1006,6 +1032,10 @@ export class ZodString extends ZodType { return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); } + cidr(options?: string | { version?: IpVersion; message?: string }) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime( options?: | string @@ -1199,6 +1229,9 @@ export class ZodString extends ZodType { get isIP() { return !!this._def.checks.find((ch) => ch.kind === "ip"); } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } get isBase64() { return !!this._def.checks.find((ch) => ch.kind === "base64"); } diff --git a/src/ZodError.ts b/src/ZodError.ts index c1f7aa3ee..6e0da79dc 100644 --- a/src/ZodError.ts +++ b/src/ZodError.ts @@ -103,6 +103,7 @@ export type StringValidation = | "time" | "duration" | "ip" + | "cidr" | "base64" | { includes: string; position?: number } | { startsWith: string } diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index f7037fcc2..ef5190062 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -372,6 +372,7 @@ test("checks getters", () => { expect(z.string().email().isUUID).toEqual(false); expect(z.string().email().isNANOID).toEqual(false); expect(z.string().email().isIP).toEqual(false); + expect(z.string().email().isCIDR).toEqual(false); expect(z.string().email().isULID).toEqual(false); expect(z.string().url().isEmail).toEqual(false); @@ -381,6 +382,7 @@ test("checks getters", () => { expect(z.string().url().isUUID).toEqual(false); expect(z.string().url().isNANOID).toEqual(false); expect(z.string().url().isIP).toEqual(false); + expect(z.string().url().isCIDR).toEqual(false); expect(z.string().url().isULID).toEqual(false); expect(z.string().cuid().isEmail).toEqual(false); @@ -390,6 +392,7 @@ test("checks getters", () => { expect(z.string().cuid().isUUID).toEqual(false); expect(z.string().cuid().isNANOID).toEqual(false); expect(z.string().cuid().isIP).toEqual(false); + expect(z.string().cuid().isCIDR).toEqual(false); expect(z.string().cuid().isULID).toEqual(false); expect(z.string().cuid2().isEmail).toEqual(false); @@ -399,6 +402,7 @@ test("checks getters", () => { expect(z.string().cuid2().isUUID).toEqual(false); expect(z.string().cuid2().isNANOID).toEqual(false); expect(z.string().cuid2().isIP).toEqual(false); + expect(z.string().cuid2().isCIDR).toEqual(false); expect(z.string().cuid2().isULID).toEqual(false); expect(z.string().uuid().isEmail).toEqual(false); @@ -408,6 +412,7 @@ test("checks getters", () => { expect(z.string().uuid().isUUID).toEqual(true); expect(z.string().uuid().isNANOID).toEqual(false); expect(z.string().uuid().isIP).toEqual(false); + expect(z.string().uuid().isCIDR).toEqual(false); expect(z.string().uuid().isULID).toEqual(false); expect(z.string().nanoid().isEmail).toEqual(false); @@ -417,6 +422,7 @@ test("checks getters", () => { expect(z.string().nanoid().isUUID).toEqual(false); expect(z.string().nanoid().isNANOID).toEqual(true); expect(z.string().nanoid().isIP).toEqual(false); + expect(z.string().nanoid().isCIDR).toEqual(false); expect(z.string().nanoid().isULID).toEqual(false); expect(z.string().ip().isEmail).toEqual(false); @@ -426,8 +432,19 @@ test("checks getters", () => { expect(z.string().ip().isUUID).toEqual(false); expect(z.string().ip().isNANOID).toEqual(false); expect(z.string().ip().isIP).toEqual(true); + expect(z.string().ip().isCIDR).toEqual(false); expect(z.string().ip().isULID).toEqual(false); + expect(z.string().cidr().isEmail).toEqual(false); + expect(z.string().cidr().isURL).toEqual(false); + expect(z.string().cidr().isCUID).toEqual(false); + expect(z.string().cidr().isCUID2).toEqual(false); + expect(z.string().cidr().isUUID).toEqual(false); + expect(z.string().cidr().isNANOID).toEqual(false); + expect(z.string().cidr().isIP).toEqual(false); + expect(z.string().cidr().isCIDR).toEqual(true); + expect(z.string().cidr().isULID).toEqual(false); + expect(z.string().ulid().isEmail).toEqual(false); expect(z.string().ulid().isURL).toEqual(false); expect(z.string().ulid().isCUID).toEqual(false); @@ -435,6 +452,7 @@ test("checks getters", () => { expect(z.string().ulid().isUUID).toEqual(false); expect(z.string().ulid().isNANOID).toEqual(false); expect(z.string().ulid().isIP).toEqual(false); + expect(z.string().ulid().isCIDR).toEqual(false); expect(z.string().ulid().isULID).toEqual(true); }); @@ -768,3 +786,45 @@ test("IP validation", () => { invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false) ).toBe(true); }); + +test("CIDR validation", () => { + const ipv4Cidr = z.string().cidr({ version: "v4" }); + expect(() => ipv4Cidr.parse("2001:0db8:85a3::8a2e:0370:7334/64")).toThrow(); + + const ipv6Cidr = z.string().cidr({ version: "v6" }); + expect(() => ipv6Cidr.parse("192.168.0.1/24")).toThrow(); + + const validCidrs = [ + "192.168.0.0/24", + "10.0.0.0/8", + "203.0.113.0/24", + "192.0.2.0/24", + "127.0.0.0/8", + "172.16.0.0/12", + "192.168.1.0/24", + "fc00::/7", + "fd00::/8", + "2001:db8::/32", + "2607:f0d0:1002:51::4/64", + "2001:0db8:85a3:0000:0000:8a2e:0370:7334/128", + "2001:0db8:1234:0000::/64", + ]; + + const invalidCidrs = [ + "192.168.1.1/33", + "10.0.0.1/-1", + "192.168.1.1/24/24", + "192.168.1.0/abc", + "2001:db8::1/129", + "2001:db8::1/-1", + "2001:db8::1/64/64", + "2001:db8::1/abc", + ]; + + // no parameters check IPv4 or IPv6 + const cidrSchema = z.string().cidr(); + expect(validCidrs.every((ip) => cidrSchema.safeParse(ip).success)).toBe(true); + expect( + invalidCidrs.every((ip) => cidrSchema.safeParse(ip).success === false) + ).toBe(true); +}); diff --git a/src/types.ts b/src/types.ts index be49c7012..df298ae3f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -565,6 +565,7 @@ export type ZodStringCheck = } | { kind: "duration"; message?: string } | { kind: "ip"; version?: IpVersion; message?: string } + | { kind: "cidr"; version?: IpVersion; message?: string } | { kind: "base64"; message?: string }; export interface ZodStringDef extends ZodTypeDef { @@ -608,11 +609,15 @@ let emojiRegex: RegExp; // faster, simpler, safer const ipv4Regex = /^(?:(?: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])$/; +const ipv4CidrRegex = + /^(?:(?: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])\/(3[0-2]|[12]?[0-9])$/; // const ipv6Regex = // /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,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}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +const ipv6CidrRegex = + /^(([0-9a-fA-F]{1,4}:){7,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}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; // https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript const base64Regex = @@ -671,6 +676,17 @@ function isValidIP(ip: string, version?: IpVersion) { return false; } +function isValidCidr(ip: string, version?: IpVersion) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + + return false; +} + export class ZodString extends ZodType { _parse(input: ParseInput): ParseReturnType { if (this._def.coerce) { @@ -933,6 +949,16 @@ export class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else if (check.kind === "base64") { if (!base64Regex.test(input.data)) { ctx = this._getOrReturnCtx(input, ctx); @@ -1006,6 +1032,10 @@ export class ZodString extends ZodType { return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); } + cidr(options?: string | { version?: IpVersion; message?: string }) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime( options?: | string @@ -1199,6 +1229,9 @@ export class ZodString extends ZodType { get isIP() { return !!this._def.checks.find((ch) => ch.kind === "ip"); } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } get isBase64() { return !!this._def.checks.find((ch) => ch.kind === "base64"); } From 71a0c33c01ca7e2be16e27f763ec1c3e9dee6943 Mon Sep 17 00:00:00 2001 From: Matt Sidor Date: Mon, 9 Dec 2024 17:40:17 -0800 Subject: [PATCH 13/30] docs: add info on unqualified local datetime strings (#3760) * Update README.md with info on unqualified local datetime strings This feature was added in version 3.23.0. (https://github.com/colinhacks/zod/releases/tag/v3.23.0) * Tweak --------- Co-authored-by: Colin McDonnell --- README.md | 7 +++++++ deno/lib/README.md | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/README.md b/README.md index c60c133fd..ad93ca2d0 100644 --- a/README.md +++ b/README.md @@ -850,6 +850,13 @@ datetime.parse("2020-01-01T00:00:00.123+02"); // pass (only offset hours) datetime.parse("2020-01-01T00:00:00Z"); // pass (Z still supported) ``` +Allow unqualified (timezone-less) datetimes with the `local` flag. + +```ts +const schema = z.string().datetime({ local: true }); +schema.parse("2020-01-01T00:00:00"); // pass +``` + You can additionally constrain the allowable `precision`. By default, arbitrary sub-second precision is supported (but optional). ```ts diff --git a/deno/lib/README.md b/deno/lib/README.md index 2f5af57c2..aeaf25dc0 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -860,6 +860,13 @@ datetime.parse("2020-01-01T00:00:00.123+02"); // pass (only offset hours) datetime.parse("2020-01-01T00:00:00Z"); // pass (Z still supported) ``` +Allow unqualified (timezone-less) datetimes with the `local` flag. + +```ts +const schema = z.string().datetime({ local: true }); +schema.parse("2020-01-01T00:00:00"); // pass +``` + You can additionally constrain the allowable `precision`. By default, arbitrary sub-second precision is supported (but optional). ```ts From b85686ab852bc75919fd9d853dfca4b0968301dd Mon Sep 17 00:00:00 2001 From: "Marvin A. Ruder" Date: Tue, 10 Dec 2024 03:16:33 +0100 Subject: [PATCH 14/30] Add support for `base64url` strings (#3712) * Add support for `base64url` strings Fixes #3711 Signed-off-by: Marvin A. Ruder * Add comments Signed-off-by: Marvin A. Ruder * Change test structure for `base64()`, `base64url()` Signed-off-by: Marvin A. Ruder * Prettier * Avoid test.each --------- Signed-off-by: Marvin A. Ruder Co-authored-by: Colin McDonnell --- ERROR_HANDLING.md | 10 +-- README.md | 1 - README_ZH.md | 1 - deno/lib/README.md | 25 ++----- deno/lib/ZodError.ts | 1 + deno/lib/__tests__/string.test.ts | 115 +++++++++++++++++++++--------- deno/lib/types.ts | 25 ++++++- src/ZodError.ts | 1 + src/__tests__/string.test.ts | 115 +++++++++++++++++++++--------- src/types.ts | 25 ++++++- 10 files changed, 225 insertions(+), 94 deletions(-) diff --git a/ERROR_HANDLING.md b/ERROR_HANDLING.md index 78dbb29ff..e41601b0e 100644 --- a/ERROR_HANDLING.md +++ b/ERROR_HANDLING.md @@ -92,10 +92,12 @@ Here's a sample Person schema. ```ts const person = z.object({ names: z.array(z.string()).nonempty(), // at least 1 name - address: z.object({ - line1: z.string(), - zipCode: z.number().min(10000), // American 5-digit code - }).strict() // do not allow unrecognized keys + address: z + .object({ + line1: z.string(), + zipCode: z.number().min(10000), // American 5-digit code + }) + .strict(), // do not allow unrecognized keys }); ``` diff --git a/README.md b/README.md index ad93ca2d0..ce4251d1f 100644 --- a/README.md +++ b/README.md @@ -614,7 +614,6 @@ bun add zod@canary # bun pnpm add zod@canary # pnpm ``` - > The rest of this README assumes you are using npm and importing directly from the `"zod"` package. ## Basic usage diff --git a/README_ZH.md b/README_ZH.md index 09bf6b111..2cb9852c9 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -326,7 +326,6 @@ bun add zod # bun pnpm add zod # pnpm ``` - > README 的剩余部分假定你是直接通过 npm 安装的`zod`包。 # 基本用法 diff --git a/deno/lib/README.md b/deno/lib/README.md index aeaf25dc0..ce4251d1f 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -65,8 +65,7 @@ - [Utilities for Zod](#utilities-for-zod) - [Installation](#installation) - [Requirements](#requirements) - - [From `npm` (Node/Bun)](#from-npm-nodebun) - - [From `deno.land/x` (Deno)](#from-denolandx-deno) + - [From `npm`](#from-npm) - [Basic usage](#basic-usage) - [Primitives](#primitives) - [Coercion for primitives](#coercion-for-primitives) @@ -81,7 +80,7 @@ - [BigInts](#bigints) - [NaNs](#nans) - [Booleans](#booleans) -- [Dates](#dates) +- [Dates](#dates-1) - [Zod enums](#zod-enums) - [Native enums](#native-enums) - [Optionals](#optionals) @@ -493,6 +492,7 @@ There are a growing number of tools that are built atop or support Zod natively! - [`tapiduck`](https://github.com/sumukhbarve/monoduck/blob/main/src/tapiduck/README.md): End-to-end typesafe JSON APIs with Zod and Express; a bit like tRPC, but simpler. - [`koa-zod-router`](https://github.com/JakeFenley/koa-zod-router): Create typesafe routes in Koa with I/O validation using Zod. - [`zod-sockets`](https://github.com/RobinTail/zod-sockets): Zod-powered Socket.IO microframework with I/O validation and built-in AsyncAPI specs +- [`oas-tszod-gen`](https://github.com/inkognitro/oas-tszod-gen): Client SDK code generator to convert OpenApi v3 specifications into TS endpoint caller functions with Zod types. #### Form integrations @@ -511,6 +511,7 @@ There are a growing number of tools that are built atop or support Zod natively! - [`mobx-zod-form`](https://github.com/MonoidDev/mobx-zod-form): Data-first form builder based on MobX & Zod. - [`@vee-validate/zod`](https://github.com/logaretm/vee-validate/tree/main/packages/zod): Form library for Vue.js with Zod schema validation. - [`zod-form-renderer`](https://github.com/thepeaklab/zod-form-renderer): Auto-infer form fields from zod schema and render them with react-hook-form with E2E type safety. +- [`antd-zod`](https://github.com/MrBr/antd-zod): Zod adapter for Ant Design form fields validation. #### Zod to X @@ -593,10 +594,11 @@ There are a growing number of tools that are built atop or support Zod natively! } ``` -### From `npm` (Node/Bun) +### From `npm` ```sh npm install zod # npm +deno add npm:zod # deno yarn add zod # yarn bun add zod # bun pnpm add zod # pnpm @@ -606,25 +608,12 @@ Zod also publishes a canary version on every commit. To install the canary: ```sh npm install zod@canary # npm +deno add npm:zod@canary # deno yarn add zod@canary # yarn bun add zod@canary # bun pnpm add zod@canary # pnpm ``` -### From `deno.land/x` (Deno) - -Unlike Node, Deno relies on direct URL imports instead of a package manager like NPM. Zod is available on [deno.land/x](https://deno.land/x). The latest version can be imported like so: - -```ts -import { z } from "https://deno.land/x/zod/mod.ts"; -``` - -You can also specify a particular version: - -```ts -import { z } from "https://deno.land/x/zod@v3.16.1/mod.ts"; -``` - > The rest of this README assumes you are using npm and importing directly from the `"zod"` package. ## Basic usage diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts index 21ad657a3..080903374 100644 --- a/deno/lib/ZodError.ts +++ b/deno/lib/ZodError.ts @@ -105,6 +105,7 @@ export type StringValidation = | "ip" | "cidr" | "base64" + | "base64url" | { includes: string; position?: number } | { startsWith: string } | { endsWith: string }; diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index 3c059f0b0..d6fec45f3 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -165,40 +165,87 @@ test("email validations", () => { ).toBe(true); }); -test("base64 validations", () => { - const validBase64Strings = [ - "SGVsbG8gV29ybGQ=", // "Hello World" - "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==", // "This is an encoded string" - "TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcms=", // "Many hands make light work" - "UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // "Patience is the key to success" - "QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // "Base64 encoding is fun" - "MTIzNDU2Nzg5MA==", // "1234567890" - "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=", // "abcdefghijklmnopqrstuvwxyz" - "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=", // "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "ISIkJSMmJyonKCk=", // "!\"#$%&'()*" - "", // Empty string is technically a valid base64 - ]; - - for (const str of validBase64Strings) { - expect(str + z.string().base64().safeParse(str).success).toBe(str + "true"); - } - - const invalidBase64Strings = [ - "12345", // Not padded correctly, not a multiple of 4 characters - "SGVsbG8gV29ybGQ", // Missing padding - "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw", // Missing padding - "!UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // Invalid character '!' - "?QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // Invalid character '?' - ".MTIzND2Nzg5MC4=", // Invalid character '.' - "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo", // Missing padding - ]; - - for (const str of invalidBase64Strings) { - expect(str + z.string().base64().safeParse(str).success).toBe( - str + "false" - ); - } -}); +const validBase64Strings = [ + "SGVsbG8gV29ybGQ=", // "Hello World" + "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==", // "This is an encoded string" + "TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcms=", // "Many hands make light work" + "UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // "Patience is the key to success" + "QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // "Base64 encoding is fun" + "MTIzNDU2Nzg5MA==", // "1234567890" + "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=", // "abcdefghijklmnopqrstuvwxyz" + "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=", // "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "ISIkJSMmJyonKCk=", // "!\"#$%&'()*" + "", // Empty string is technically valid base64 + "w7/Dv8O+w74K", // ÿÿþþ +]; + +for (const str of validBase64Strings) { + test(`base64 should accept ${str}`, () => { + expect(z.string().base64().safeParse(str).success).toBe(true); + }); +} + +const invalidBase64Strings = [ + "12345", // Not padded correctly, not a multiple of 4 characters + "12345===", // Not padded correctly + "SGVsbG8gV29ybGQ", // Missing padding + "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw", // Missing padding + "!UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // Invalid character '!' + "?QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // Invalid character '?' + ".MTIzND2Nzg5MC4=", // Invalid character '.' + "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo", // Missing padding + "w7_Dv8O-w74K", // Has - and _ characters (is base64url) +]; + +for (const str of invalidBase64Strings) { + test(`base64 should reject ${str}`, () => { + expect(z.string().base64().safeParse(str).success).toBe(false); + }); +} + +const validBase64URLStrings = [ + "SGVsbG8gV29ybGQ", // "Hello World" + "SGVsbG8gV29ybGQ=", // "Hello World" with padding + "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw", // "This is an encoded string" + "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==", // "This is an encoded string" with padding + "TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcms", // "Many hands make light work" + "TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcms=", // "Many hands make light work" with padding + "UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // "Patience is the key to success" + "QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg", // "Base64 encoding is fun" + "QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // "Base64 encoding is fun" with padding + "MTIzNDU2Nzg5MA", // "1234567890" + "MTIzNDU2Nzg5MA==", // "1234567890" with padding + "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo", // "abcdefghijklmnopqrstuvwxyz" + "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=", // "abcdefghijklmnopqrstuvwxyz with padding" + "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo", // "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=", // "ABCDEFGHIJKLMNOPQRSTUVWXYZ" with padding + "ISIkJSMmJyonKCk", // "!\"#$%&'()*" + "ISIkJSMmJyonKCk=", // "!\"#$%&'()*" with padding + "", // Empty string is technically valid base64url + "w7_Dv8O-w74K", // ÿÿþþ + "123456", +]; + +for (const str of validBase64URLStrings) { + test(`base64url should accept ${str}`, () => { + expect(z.string().base64url().safeParse(str).success).toBe(true); + }); +} + +const invalidBase64URLStrings = [ + "w7/Dv8O+w74K", // Has + and / characters (is base64) + "12345", // Invalid length (not a multiple of 4 characters when adding allowed number of padding characters) + "12345===", // Not padded correctly + "!UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // Invalid character '!' + "?QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // Invalid character '?' + ".MTIzND2Nzg5MC4=", // Invalid character '.' +]; + +for (const str of invalidBase64URLStrings) { + test(`base64url should reject ${str}`, () => { + expect(z.string().base64url().safeParse(str).success).toBe(false); + }); +} test("url validations", () => { const url = z.string().url(); diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 42d2606bb..19bbdd6c9 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -566,7 +566,8 @@ export type ZodStringCheck = | { kind: "duration"; message?: string } | { kind: "ip"; version?: IpVersion; message?: string } | { kind: "cidr"; version?: IpVersion; message?: string } - | { kind: "base64"; message?: string }; + | { kind: "base64"; message?: string } + | { kind: "base64url"; message?: string }; export interface ZodStringDef extends ZodTypeDef { checks: ZodStringCheck[]; @@ -623,6 +624,10 @@ const ipv6CidrRegex = const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +// https://base64.guru/standards/base64url +const base64urlRegex = + /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; + // simple // const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; // no leap year validation @@ -969,6 +974,16 @@ export class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "base64url") { + if (!base64urlRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else { util.assertNever(check); } @@ -1027,6 +1042,10 @@ export class ZodString extends ZodType { base64(message?: errorUtil.ErrMessage) { return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); } + base64url(message?: errorUtil.ErrMessage) { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return this._addCheck({ kind: "base64url", ...errorUtil.errToObj(message) }); + } ip(options?: string | { version?: IpVersion; message?: string }) { return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); @@ -1235,6 +1254,10 @@ export class ZodString extends ZodType { get isBase64() { return !!this._def.checks.find((ch) => ch.kind === "base64"); } + get isBase64url() { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return !!this._def.checks.find((ch) => ch.kind === "base64url"); + } get minLength() { let min: number | null = null; diff --git a/src/ZodError.ts b/src/ZodError.ts index 6e0da79dc..1511c412a 100644 --- a/src/ZodError.ts +++ b/src/ZodError.ts @@ -105,6 +105,7 @@ export type StringValidation = | "ip" | "cidr" | "base64" + | "base64url" | { includes: string; position?: number } | { startsWith: string } | { endsWith: string }; diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index ef5190062..4b785c21c 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -164,40 +164,87 @@ test("email validations", () => { ).toBe(true); }); -test("base64 validations", () => { - const validBase64Strings = [ - "SGVsbG8gV29ybGQ=", // "Hello World" - "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==", // "This is an encoded string" - "TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcms=", // "Many hands make light work" - "UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // "Patience is the key to success" - "QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // "Base64 encoding is fun" - "MTIzNDU2Nzg5MA==", // "1234567890" - "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=", // "abcdefghijklmnopqrstuvwxyz" - "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=", // "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "ISIkJSMmJyonKCk=", // "!\"#$%&'()*" - "", // Empty string is technically a valid base64 - ]; - - for (const str of validBase64Strings) { - expect(str + z.string().base64().safeParse(str).success).toBe(str + "true"); - } - - const invalidBase64Strings = [ - "12345", // Not padded correctly, not a multiple of 4 characters - "SGVsbG8gV29ybGQ", // Missing padding - "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw", // Missing padding - "!UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // Invalid character '!' - "?QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // Invalid character '?' - ".MTIzND2Nzg5MC4=", // Invalid character '.' - "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo", // Missing padding - ]; - - for (const str of invalidBase64Strings) { - expect(str + z.string().base64().safeParse(str).success).toBe( - str + "false" - ); - } -}); +const validBase64Strings = [ + "SGVsbG8gV29ybGQ=", // "Hello World" + "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==", // "This is an encoded string" + "TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcms=", // "Many hands make light work" + "UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // "Patience is the key to success" + "QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // "Base64 encoding is fun" + "MTIzNDU2Nzg5MA==", // "1234567890" + "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=", // "abcdefghijklmnopqrstuvwxyz" + "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=", // "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "ISIkJSMmJyonKCk=", // "!\"#$%&'()*" + "", // Empty string is technically valid base64 + "w7/Dv8O+w74K", // ÿÿþþ +]; + +for (const str of validBase64Strings) { + test(`base64 should accept ${str}`, () => { + expect(z.string().base64().safeParse(str).success).toBe(true); + }); +} + +const invalidBase64Strings = [ + "12345", // Not padded correctly, not a multiple of 4 characters + "12345===", // Not padded correctly + "SGVsbG8gV29ybGQ", // Missing padding + "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw", // Missing padding + "!UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // Invalid character '!' + "?QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // Invalid character '?' + ".MTIzND2Nzg5MC4=", // Invalid character '.' + "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo", // Missing padding + "w7_Dv8O-w74K", // Has - and _ characters (is base64url) +]; + +for (const str of invalidBase64Strings) { + test(`base64 should reject ${str}`, () => { + expect(z.string().base64().safeParse(str).success).toBe(false); + }); +} + +const validBase64URLStrings = [ + "SGVsbG8gV29ybGQ", // "Hello World" + "SGVsbG8gV29ybGQ=", // "Hello World" with padding + "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw", // "This is an encoded string" + "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==", // "This is an encoded string" with padding + "TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcms", // "Many hands make light work" + "TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcms=", // "Many hands make light work" with padding + "UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // "Patience is the key to success" + "QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg", // "Base64 encoding is fun" + "QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // "Base64 encoding is fun" with padding + "MTIzNDU2Nzg5MA", // "1234567890" + "MTIzNDU2Nzg5MA==", // "1234567890" with padding + "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo", // "abcdefghijklmnopqrstuvwxyz" + "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=", // "abcdefghijklmnopqrstuvwxyz with padding" + "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo", // "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=", // "ABCDEFGHIJKLMNOPQRSTUVWXYZ" with padding + "ISIkJSMmJyonKCk", // "!\"#$%&'()*" + "ISIkJSMmJyonKCk=", // "!\"#$%&'()*" with padding + "", // Empty string is technically valid base64url + "w7_Dv8O-w74K", // ÿÿþþ + "123456", +]; + +for (const str of validBase64URLStrings) { + test(`base64url should accept ${str}`, () => { + expect(z.string().base64url().safeParse(str).success).toBe(true); + }); +} + +const invalidBase64URLStrings = [ + "w7/Dv8O+w74K", // Has + and / characters (is base64) + "12345", // Invalid length (not a multiple of 4 characters when adding allowed number of padding characters) + "12345===", // Not padded correctly + "!UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // Invalid character '!' + "?QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // Invalid character '?' + ".MTIzND2Nzg5MC4=", // Invalid character '.' +]; + +for (const str of invalidBase64URLStrings) { + test(`base64url should reject ${str}`, () => { + expect(z.string().base64url().safeParse(str).success).toBe(false); + }); +} test("url validations", () => { const url = z.string().url(); diff --git a/src/types.ts b/src/types.ts index df298ae3f..bbb857a47 100644 --- a/src/types.ts +++ b/src/types.ts @@ -566,7 +566,8 @@ export type ZodStringCheck = | { kind: "duration"; message?: string } | { kind: "ip"; version?: IpVersion; message?: string } | { kind: "cidr"; version?: IpVersion; message?: string } - | { kind: "base64"; message?: string }; + | { kind: "base64"; message?: string } + | { kind: "base64url"; message?: string }; export interface ZodStringDef extends ZodTypeDef { checks: ZodStringCheck[]; @@ -623,6 +624,10 @@ const ipv6CidrRegex = const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +// https://base64.guru/standards/base64url +const base64urlRegex = + /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; + // simple // const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; // no leap year validation @@ -969,6 +974,16 @@ export class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "base64url") { + if (!base64urlRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else { util.assertNever(check); } @@ -1027,6 +1042,10 @@ export class ZodString extends ZodType { base64(message?: errorUtil.ErrMessage) { return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); } + base64url(message?: errorUtil.ErrMessage) { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return this._addCheck({ kind: "base64url", ...errorUtil.errToObj(message) }); + } ip(options?: string | { version?: IpVersion; message?: string }) { return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); @@ -1235,6 +1254,10 @@ export class ZodString extends ZodType { get isBase64() { return !!this._def.checks.find((ch) => ch.kind === "base64"); } + get isBase64url() { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return !!this._def.checks.find((ch) => ch.kind === "base64url"); + } get minLength() { let min: number | null = null; From 6407bed5a229f330b9353e086f7798f1422e2bb7 Mon Sep 17 00:00:00 2001 From: Andrew Haines Date: Tue, 10 Dec 2024 02:18:09 +0000 Subject: [PATCH 15/30] Allow creation of discriminated unions with a readonly array of options (#3535) Signed-off-by: Andrew Haines --- deno/lib/__tests__/discriminated-unions.test.ts | 11 +++++++++++ deno/lib/types.ts | 6 +++--- src/__tests__/discriminated-unions.test.ts | 11 +++++++++++ src/types.ts | 6 +++--- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/deno/lib/__tests__/discriminated-unions.test.ts b/deno/lib/__tests__/discriminated-unions.test.ts index bff0cdac2..bf090db1f 100644 --- a/deno/lib/__tests__/discriminated-unions.test.ts +++ b/deno/lib/__tests__/discriminated-unions.test.ts @@ -308,3 +308,14 @@ test("optional and nullable", () => { if (value.key === "b") value.b; if (value.key === null) value.b; }); + +test("readonly array of options", () => { + const options = [ + z.object({ type: z.literal("x"), val: z.literal(1) }), + z.object({ type: z.literal("y"), val: z.literal(2) }), + ] as const; + + expect( + z.discriminatedUnion("type", options).parse({ type: "x", val: 1 }) + ).toEqual({ type: "x", val: 1 }); +}); diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 19bbdd6c9..1fed75de2 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -3165,7 +3165,7 @@ export type ZodDiscriminatedUnionOption = export interface ZodDiscriminatedUnionDef< Discriminator extends string, - Options extends ZodDiscriminatedUnionOption[] = ZodDiscriminatedUnionOption[] + Options extends readonly ZodDiscriminatedUnionOption[] = ZodDiscriminatedUnionOption[] > extends ZodTypeDef { discriminator: Discriminator; options: Options; @@ -3175,7 +3175,7 @@ export interface ZodDiscriminatedUnionDef< export class ZodDiscriminatedUnion< Discriminator extends string, - Options extends ZodDiscriminatedUnionOption[] + Options extends readonly ZodDiscriminatedUnionOption[] > extends ZodType< output, ZodDiscriminatedUnionDef, @@ -3245,7 +3245,7 @@ export class ZodDiscriminatedUnion< */ static create< Discriminator extends string, - Types extends [ + Types extends readonly [ ZodDiscriminatedUnionOption, ...ZodDiscriminatedUnionOption[] ] diff --git a/src/__tests__/discriminated-unions.test.ts b/src/__tests__/discriminated-unions.test.ts index 9d8e7b6ce..58698e7be 100644 --- a/src/__tests__/discriminated-unions.test.ts +++ b/src/__tests__/discriminated-unions.test.ts @@ -307,3 +307,14 @@ test("optional and nullable", () => { if (value.key === "b") value.b; if (value.key === null) value.b; }); + +test("readonly array of options", () => { + const options = [ + z.object({ type: z.literal("x"), val: z.literal(1) }), + z.object({ type: z.literal("y"), val: z.literal(2) }), + ] as const; + + expect( + z.discriminatedUnion("type", options).parse({ type: "x", val: 1 }) + ).toEqual({ type: "x", val: 1 }); +}); diff --git a/src/types.ts b/src/types.ts index bbb857a47..24e12299e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3165,7 +3165,7 @@ export type ZodDiscriminatedUnionOption = export interface ZodDiscriminatedUnionDef< Discriminator extends string, - Options extends ZodDiscriminatedUnionOption[] = ZodDiscriminatedUnionOption[] + Options extends readonly ZodDiscriminatedUnionOption[] = ZodDiscriminatedUnionOption[] > extends ZodTypeDef { discriminator: Discriminator; options: Options; @@ -3175,7 +3175,7 @@ export interface ZodDiscriminatedUnionDef< export class ZodDiscriminatedUnion< Discriminator extends string, - Options extends ZodDiscriminatedUnionOption[] + Options extends readonly ZodDiscriminatedUnionOption[] > extends ZodType< output, ZodDiscriminatedUnionDef, @@ -3245,7 +3245,7 @@ export class ZodDiscriminatedUnion< */ static create< Discriminator extends string, - Types extends [ + Types extends readonly [ ZodDiscriminatedUnionOption, ...ZodDiscriminatedUnionOption[] ] From 37551462f4a534f86e6190aafea1747b010eca7a Mon Sep 17 00:00:00 2001 From: EthanBehrends Date: Mon, 9 Dec 2024 18:18:59 -0800 Subject: [PATCH 16/30] Remove createParams cascade from .array() (#3530) --- deno/lib/__tests__/description.test.ts | 8 ++++++++ deno/lib/types.ts | 2 +- src/__tests__/description.test.ts | 8 ++++++++ src/types.ts | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/deno/lib/__tests__/description.test.ts b/deno/lib/__tests__/description.test.ts index 63015eab9..b19018a5c 100644 --- a/deno/lib/__tests__/description.test.ts +++ b/deno/lib/__tests__/description.test.ts @@ -26,3 +26,11 @@ test("description should carry over to chained schemas", () => { description ); }); + +test("description should not carry over to chained array schema", () => { + const schema = z.string().describe(description) + + expect(schema.description).toEqual(description); + expect(schema.array().description).toEqual(undefined); + expect(z.array(schema).description).toEqual(undefined); +}) diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 1fed75de2..3bb8b65e0 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -434,7 +434,7 @@ export abstract class ZodType< return this.nullable().optional(); } array(): ZodArray { - return ZodArray.create(this, this._def); + return ZodArray.create(this); } promise(): ZodPromise { return ZodPromise.create(this, this._def); diff --git a/src/__tests__/description.test.ts b/src/__tests__/description.test.ts index 5a8d73dc8..68187238c 100644 --- a/src/__tests__/description.test.ts +++ b/src/__tests__/description.test.ts @@ -25,3 +25,11 @@ test("description should carry over to chained schemas", () => { description ); }); + +test("description should not carry over to chained array schema", () => { + const schema = z.string().describe(description) + + expect(schema.description).toEqual(description); + expect(schema.array().description).toEqual(undefined); + expect(z.array(schema).description).toEqual(undefined); +}) diff --git a/src/types.ts b/src/types.ts index 24e12299e..5f8c65d04 100644 --- a/src/types.ts +++ b/src/types.ts @@ -434,7 +434,7 @@ export abstract class ZodType< return this.nullable().optional(); } array(): ZodArray { - return ZodArray.create(this, this._def); + return ZodArray.create(this); } promise(): ZodPromise { return ZodPromise.create(this, this._def); From 963386df253360fde67ca10c6bf47fec1fcc476a Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Mon, 9 Dec 2024 18:19:59 -0800 Subject: [PATCH 17/30] Fix lint --- deno/lib/__tests__/description.test.ts | 4 ++-- src/__tests__/description.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deno/lib/__tests__/description.test.ts b/deno/lib/__tests__/description.test.ts index b19018a5c..9a9292e44 100644 --- a/deno/lib/__tests__/description.test.ts +++ b/deno/lib/__tests__/description.test.ts @@ -28,9 +28,9 @@ test("description should carry over to chained schemas", () => { }); test("description should not carry over to chained array schema", () => { - const schema = z.string().describe(description) + const schema = z.string().describe(description); expect(schema.description).toEqual(description); expect(schema.array().description).toEqual(undefined); expect(z.array(schema).description).toEqual(undefined); -}) +}); diff --git a/src/__tests__/description.test.ts b/src/__tests__/description.test.ts index 68187238c..88cff471b 100644 --- a/src/__tests__/description.test.ts +++ b/src/__tests__/description.test.ts @@ -27,9 +27,9 @@ test("description should carry over to chained schemas", () => { }); test("description should not carry over to chained array schema", () => { - const schema = z.string().describe(description) + const schema = z.string().describe(description); expect(schema.description).toEqual(description); expect(schema.array().description).toEqual(undefined); expect(z.array(schema).description).toEqual(undefined); -}) +}); From 69a1798ce2df65555bda0a8978a6baadd7d5588e Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Mon, 9 Dec 2024 18:53:00 -0800 Subject: [PATCH 18/30] Implement Standard Schema spec (#3850) * Implement Standard Schema * Remove dep * WIP * Fix CI * Update to latest standard-schema * Add standard-schema/spec as devDep --- deno/lib/__tests__/standard-schema.test.ts | 84 +++++++++++++++ deno/lib/standard-schema.ts | 119 +++++++++++++++++++++ deno/lib/types.ts | 60 ++++++++++- package.json | 23 +++- playground.ts | 13 +++ src/__tests__/standard-schema.test.ts | 83 ++++++++++++++ src/standard-schema.ts | 119 +++++++++++++++++++++ src/types.ts | 60 ++++++++++- yarn.lock | 5 + 9 files changed, 560 insertions(+), 6 deletions(-) create mode 100644 deno/lib/__tests__/standard-schema.test.ts create mode 100644 deno/lib/standard-schema.ts create mode 100644 src/__tests__/standard-schema.test.ts create mode 100644 src/standard-schema.ts diff --git a/deno/lib/__tests__/standard-schema.test.ts b/deno/lib/__tests__/standard-schema.test.ts new file mode 100644 index 000000000..065315c52 --- /dev/null +++ b/deno/lib/__tests__/standard-schema.test.ts @@ -0,0 +1,84 @@ +// @ts-ignore TS6133 +import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts"; +const test = Deno.test; +import { util } from "../helpers/util.ts"; + +import * as z from "../index.ts"; + +import type { StandardSchemaV1 } from "@standard-schema/spec"; + +test("assignability", () => { + const _s1: StandardSchemaV1 = z.string(); + const _s2: StandardSchemaV1 = z.string(); + const _s3: StandardSchemaV1 = z.string(); + const _s4: StandardSchemaV1 = z.string(); + [_s1, _s2, _s3, _s4]; +}); + +test("type inference", () => { + const stringToNumber = z.string().transform((x) => x.length); + type input = StandardSchemaV1.InferInput; + util.assertEqual(true); + type output = StandardSchemaV1.InferOutput; + util.assertEqual(true); +}); + +test("valid parse", () => { + const schema = z.string(); + const result = schema["~standard"]["validate"]("hello"); + if (result instanceof Promise) { + throw new Error("Expected sync result"); + } + expect(result.issues).toEqual(undefined); + if (result.issues) { + throw new Error("Expected no issues"); + } else { + expect(result.value).toEqual("hello"); + } +}); + +test("invalid parse", () => { + const schema = z.string(); + const result = schema["~standard"]["validate"](1234); + if (result instanceof Promise) { + throw new Error("Expected sync result"); + } + expect(result.issues).toBeDefined(); + if (!result.issues) { + throw new Error("Expected issues"); + } + expect(result.issues.length).toEqual(1); + expect(result.issues[0].path).toEqual([]); +}); + +test("valid parse async", async () => { + const schema = z.string().refine(async () => true); + const _result = schema["~standard"]["validate"]("hello"); + if (_result instanceof Promise) { + const result = await _result; + expect(result.issues).toEqual(undefined); + if (result.issues) { + throw new Error("Expected no issues"); + } else { + expect(result.value).toEqual("hello"); + } + } else { + throw new Error("Expected async result"); + } +}); + +test("invalid parse async", async () => { + const schema = z.string().refine(async () => false); + const _result = schema["~standard"]["validate"]("hello"); + if (_result instanceof Promise) { + const result = await _result; + expect(result.issues).toBeDefined(); + if (!result.issues) { + throw new Error("Expected issues"); + } + expect(result.issues.length).toEqual(1); + expect(result.issues[0].path).toEqual([]); + } else { + throw new Error("Expected async result"); + } +}); diff --git a/deno/lib/standard-schema.ts b/deno/lib/standard-schema.ts new file mode 100644 index 000000000..111888e57 --- /dev/null +++ b/deno/lib/standard-schema.ts @@ -0,0 +1,119 @@ +/** + * The Standard Schema interface. + */ +export type StandardSchemaV1 = { + /** + * The Standard Schema properties. + */ + readonly "~standard": StandardSchemaV1.Props; +}; + +export declare namespace StandardSchemaV1 { + /** + * The Standard Schema properties interface. + */ + export interface Props { + /** + * The version number of the standard. + */ + readonly version: 1; + /** + * The vendor name of the schema library. + */ + readonly vendor: string; + /** + * Validates unknown input values. + */ + readonly validate: ( + value: unknown + ) => Result | Promise>; + /** + * Inferred types associated with the schema. + */ + readonly types?: Types | undefined; + } + + /** + * The result interface of the validate function. + */ + export type Result = SuccessResult | FailureResult; + + /** + * The result interface if validation succeeds. + */ + export interface SuccessResult { + /** + * The typed output value. + */ + readonly value: Output; + /** + * The non-existent issues. + */ + readonly issues?: undefined; + } + + /** + * The result interface if validation fails. + */ + export interface FailureResult { + /** + * The issues of failed validation. + */ + readonly issues: ReadonlyArray; + } + + /** + * The issue interface of the failure output. + */ + export interface Issue { + /** + * The error message of the issue. + */ + readonly message: string; + /** + * The path of the issue, if any. + */ + readonly path?: ReadonlyArray | undefined; + } + + /** + * The path segment interface of the issue. + */ + export interface PathSegment { + /** + * The key representing a path segment. + */ + readonly key: PropertyKey; + } + + /** + * The Standard Schema types interface. + */ + export interface Types { + /** + * The input type of the schema. + */ + readonly input: Input; + /** + * The output type of the schema. + */ + readonly output: Output; + } + + /** + * Infers the input type of a Standard Schema. + */ + export type InferInput = NonNullable< + Schema["~standard"]["types"] + >["input"]; + + /** + * Infers the output type of a Standard Schema. + */ + export type InferOutput = NonNullable< + Schema["~standard"]["types"] + >["output"]; + + // biome-ignore lint/complexity/noUselessEmptyExport: needed for granular visibility control of TS namespace + export {}; +} diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 3bb8b65e0..5f366b236 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -23,6 +23,7 @@ import { import { partialUtil } from "./helpers/partialUtil.ts"; import { Primitive } from "./helpers/typeAliases.ts"; import { getParsedType, objectUtil, util, ZodParsedType } from "./helpers/util.ts"; +import type { StandardSchemaV1 } from "./standard-schema.ts"; import { IssueData, StringValidation, @@ -169,7 +170,8 @@ export abstract class ZodType< Output = any, Def extends ZodTypeDef = ZodTypeDef, Input = Output -> { +> implements StandardSchemaV1 +{ readonly _type!: Output; readonly _output!: Output; readonly _input!: Input; @@ -179,6 +181,8 @@ export abstract class ZodType< return this._def.description; } + "~standard": StandardSchemaV1.Props; + abstract _parse(input: ParseInput): ParseReturnType; _getType(input: ParseInput): string { @@ -262,6 +266,55 @@ export abstract class ZodType< return handleResult(ctx, result); } + "~validate"( + data: unknown + ): + | StandardSchemaV1.Result + | Promise> { + const ctx: ParseContext = { + common: { + issues: [], + async: !!(this["~standard"] as any).async, + }, + path: [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + + if (!(this["~standard"] as any).async) { + try { + const result = this._parseSync({ data, path: [], parent: ctx }); + return isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }; + } catch (err: any) { + if ((err as Error)?.message?.toLowerCase()?.includes("encountered")) { + (this["~standard"] as any).async = true; + } + (ctx as any).common = { + issues: [], + async: true, + }; + } + } + + return this._parseAsync({ data, path: [], parent: ctx }).then((result) => + isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + } + ); + } + async parseAsync( data: unknown, params?: Partial @@ -422,6 +475,11 @@ export abstract class ZodType< this.readonly = this.readonly.bind(this); this.isNullable = this.isNullable.bind(this); this.isOptional = this.isOptional.bind(this); + this["~standard"] = { + version: 1, + vendor: "zod", + validate: (data) => this["~validate"](data), + }; } optional(): ZodOptional { diff --git a/package.json b/package.json index 639030673..d6ecb8918 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@babel/preset-typescript": "^7.22.5", "@jest/globals": "^29.4.3", "@rollup/plugin-typescript": "^8.2.0", + "@standard-schema/spec": "^1.0.0-beta.4", "@swc/core": "^1.3.66", "@swc/jest": "^0.2.26", "@types/benchmark": "^2.1.0", @@ -59,14 +60,28 @@ "url": "https://github.com/colinhacks/zod/issues" }, "description": "TypeScript-first schema declaration and validation library with static type inference", - "files": ["/lib", "/index.d.ts"], + "files": [ + "/lib", + "/index.d.ts" + ], "funding": "https://github.com/sponsors/colinhacks", "homepage": "https://zod.dev", - "keywords": ["typescript", "schema", "validation", "type", "inference"], + "keywords": [ + "typescript", + "schema", + "validation", + "type", + "inference" + ], "license": "MIT", "lint-staged": { - "src/*.ts": ["eslint --cache --fix", "prettier --ignore-unknown --write"], - "*.md": ["prettier --ignore-unknown --write"] + "src/*.ts": [ + "eslint --cache --fix", + "prettier --ignore-unknown --write" + ], + "*.md": [ + "prettier --ignore-unknown --write" + ] }, "scripts": { "prettier:check": "prettier --check src/**/*.ts deno/lib/**/*.ts *.md --no-error-on-unmatched-pattern", diff --git a/playground.ts b/playground.ts index 4e01473b6..bca434a98 100644 --- a/playground.ts +++ b/playground.ts @@ -1,3 +1,16 @@ import { z } from "./src"; z; + +const schema = z + .string() + .transform((input) => input || undefined) + .optional() + .default("default"); + +type Input = z.input; // string | undefined +type Output = z.output; // string + +const result = schema.safeParse(""); + +console.log(result); // { success: true, data: undefined } diff --git a/src/__tests__/standard-schema.test.ts b/src/__tests__/standard-schema.test.ts new file mode 100644 index 000000000..8f67cce76 --- /dev/null +++ b/src/__tests__/standard-schema.test.ts @@ -0,0 +1,83 @@ +// @ts-ignore TS6133 +import { expect, test } from "@jest/globals"; +import { util } from "../helpers/util"; + +import * as z from "../index"; + +import type { StandardSchemaV1 } from "@standard-schema/spec"; + +test("assignability", () => { + const _s1: StandardSchemaV1 = z.string(); + const _s2: StandardSchemaV1 = z.string(); + const _s3: StandardSchemaV1 = z.string(); + const _s4: StandardSchemaV1 = z.string(); + [_s1, _s2, _s3, _s4]; +}); + +test("type inference", () => { + const stringToNumber = z.string().transform((x) => x.length); + type input = StandardSchemaV1.InferInput; + util.assertEqual(true); + type output = StandardSchemaV1.InferOutput; + util.assertEqual(true); +}); + +test("valid parse", () => { + const schema = z.string(); + const result = schema["~standard"]["validate"]("hello"); + if (result instanceof Promise) { + throw new Error("Expected sync result"); + } + expect(result.issues).toEqual(undefined); + if (result.issues) { + throw new Error("Expected no issues"); + } else { + expect(result.value).toEqual("hello"); + } +}); + +test("invalid parse", () => { + const schema = z.string(); + const result = schema["~standard"]["validate"](1234); + if (result instanceof Promise) { + throw new Error("Expected sync result"); + } + expect(result.issues).toBeDefined(); + if (!result.issues) { + throw new Error("Expected issues"); + } + expect(result.issues.length).toEqual(1); + expect(result.issues[0].path).toEqual([]); +}); + +test("valid parse async", async () => { + const schema = z.string().refine(async () => true); + const _result = schema["~standard"]["validate"]("hello"); + if (_result instanceof Promise) { + const result = await _result; + expect(result.issues).toEqual(undefined); + if (result.issues) { + throw new Error("Expected no issues"); + } else { + expect(result.value).toEqual("hello"); + } + } else { + throw new Error("Expected async result"); + } +}); + +test("invalid parse async", async () => { + const schema = z.string().refine(async () => false); + const _result = schema["~standard"]["validate"]("hello"); + if (_result instanceof Promise) { + const result = await _result; + expect(result.issues).toBeDefined(); + if (!result.issues) { + throw new Error("Expected issues"); + } + expect(result.issues.length).toEqual(1); + expect(result.issues[0].path).toEqual([]); + } else { + throw new Error("Expected async result"); + } +}); diff --git a/src/standard-schema.ts b/src/standard-schema.ts new file mode 100644 index 000000000..111888e57 --- /dev/null +++ b/src/standard-schema.ts @@ -0,0 +1,119 @@ +/** + * The Standard Schema interface. + */ +export type StandardSchemaV1 = { + /** + * The Standard Schema properties. + */ + readonly "~standard": StandardSchemaV1.Props; +}; + +export declare namespace StandardSchemaV1 { + /** + * The Standard Schema properties interface. + */ + export interface Props { + /** + * The version number of the standard. + */ + readonly version: 1; + /** + * The vendor name of the schema library. + */ + readonly vendor: string; + /** + * Validates unknown input values. + */ + readonly validate: ( + value: unknown + ) => Result | Promise>; + /** + * Inferred types associated with the schema. + */ + readonly types?: Types | undefined; + } + + /** + * The result interface of the validate function. + */ + export type Result = SuccessResult | FailureResult; + + /** + * The result interface if validation succeeds. + */ + export interface SuccessResult { + /** + * The typed output value. + */ + readonly value: Output; + /** + * The non-existent issues. + */ + readonly issues?: undefined; + } + + /** + * The result interface if validation fails. + */ + export interface FailureResult { + /** + * The issues of failed validation. + */ + readonly issues: ReadonlyArray; + } + + /** + * The issue interface of the failure output. + */ + export interface Issue { + /** + * The error message of the issue. + */ + readonly message: string; + /** + * The path of the issue, if any. + */ + readonly path?: ReadonlyArray | undefined; + } + + /** + * The path segment interface of the issue. + */ + export interface PathSegment { + /** + * The key representing a path segment. + */ + readonly key: PropertyKey; + } + + /** + * The Standard Schema types interface. + */ + export interface Types { + /** + * The input type of the schema. + */ + readonly input: Input; + /** + * The output type of the schema. + */ + readonly output: Output; + } + + /** + * Infers the input type of a Standard Schema. + */ + export type InferInput = NonNullable< + Schema["~standard"]["types"] + >["input"]; + + /** + * Infers the output type of a Standard Schema. + */ + export type InferOutput = NonNullable< + Schema["~standard"]["types"] + >["output"]; + + // biome-ignore lint/complexity/noUselessEmptyExport: needed for granular visibility control of TS namespace + export {}; +} diff --git a/src/types.ts b/src/types.ts index 5f8c65d04..34ee9cb79 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,6 +23,7 @@ import { import { partialUtil } from "./helpers/partialUtil"; import { Primitive } from "./helpers/typeAliases"; import { getParsedType, objectUtil, util, ZodParsedType } from "./helpers/util"; +import type { StandardSchemaV1 } from "./standard-schema"; import { IssueData, StringValidation, @@ -169,7 +170,8 @@ export abstract class ZodType< Output = any, Def extends ZodTypeDef = ZodTypeDef, Input = Output -> { +> implements StandardSchemaV1 +{ readonly _type!: Output; readonly _output!: Output; readonly _input!: Input; @@ -179,6 +181,8 @@ export abstract class ZodType< return this._def.description; } + "~standard": StandardSchemaV1.Props; + abstract _parse(input: ParseInput): ParseReturnType; _getType(input: ParseInput): string { @@ -262,6 +266,55 @@ export abstract class ZodType< return handleResult(ctx, result); } + "~validate"( + data: unknown + ): + | StandardSchemaV1.Result + | Promise> { + const ctx: ParseContext = { + common: { + issues: [], + async: !!(this["~standard"] as any).async, + }, + path: [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + + if (!(this["~standard"] as any).async) { + try { + const result = this._parseSync({ data, path: [], parent: ctx }); + return isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }; + } catch (err: any) { + if ((err as Error)?.message?.toLowerCase()?.includes("encountered")) { + (this["~standard"] as any).async = true; + } + (ctx as any).common = { + issues: [], + async: true, + }; + } + } + + return this._parseAsync({ data, path: [], parent: ctx }).then((result) => + isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + } + ); + } + async parseAsync( data: unknown, params?: Partial @@ -422,6 +475,11 @@ export abstract class ZodType< this.readonly = this.readonly.bind(this); this.isNullable = this.isNullable.bind(this); this.isOptional = this.isOptional.bind(this); + this["~standard"] = { + version: 1, + vendor: "zod", + validate: (data) => this["~validate"](data), + }; } optional(): ZodOptional { diff --git a/yarn.lock b/yarn.lock index 5c18678b4..2016a4c07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2881,6 +2881,11 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@standard-schema/spec@^1.0.0-beta.4": + version "1.0.0-beta.4" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0-beta.4.tgz#62f520109add3eb016004098363bfee0678dd1ec" + integrity sha512-d3IxtzLo7P1oZ8s8YNvxzBUXRXojSut8pbPrTYtzsc5sn4+53jVqbk66pQerSZbZSJZQux6LkclB/+8IDordHg== + "@swc/core-darwin-arm64@1.4.8": version "1.4.8" resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.8.tgz#2fb702e209310c2da2bc712b0757c011b583a60d" From c1dd537baa9e4fad781ea365643399707fea91be Mon Sep 17 00:00:00 2001 From: Schalk Venter Date: Tue, 10 Dec 2024 05:01:07 +0200 Subject: [PATCH 19/30] Adds `frrm` package to documentation (#3818) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ce4251d1f..83af7ed6b 100644 --- a/README.md +++ b/README.md @@ -512,6 +512,7 @@ There are a growing number of tools that are built atop or support Zod natively! - [`@vee-validate/zod`](https://github.com/logaretm/vee-validate/tree/main/packages/zod): Form library for Vue.js with Zod schema validation. - [`zod-form-renderer`](https://github.com/thepeaklab/zod-form-renderer): Auto-infer form fields from zod schema and render them with react-hook-form with E2E type safety. - [`antd-zod`](https://github.com/MrBr/antd-zod): Zod adapter for Ant Design form fields validation. +- [`frrm`](https://github.com/schalkventer/frrm): Tiny 0.5kb Zod-based, HTML form abstraction that goes brr. #### Zod to X From b68c05fea12d8060000aa06abc1e95b08f061378 Mon Sep 17 00:00:00 2001 From: Mokshit Jain Date: Tue, 10 Dec 2024 11:32:49 +0530 Subject: [PATCH 20/30] feat: Add JWT string validator (#3893) * feat: add JWT string validator - Add z.string().jwt() validator for checking JWT format - Add optional algorithm validation with z.string().jwt({ alg: string }) - Implement in both main and Deno versions - Add comprehensive test coverage - Use atob() for browser compatibility * fix: rename algorithm to alg --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- deno/lib/ZodError.ts | 1 + deno/lib/__tests__/string.test.ts | 38 +++++++++++++++++++++++++++++++ deno/lib/types.ts | 35 ++++++++++++++++++++++++++++ src/ZodError.ts | 1 + src/__tests__/string.test.ts | 38 +++++++++++++++++++++++++++++++ src/types.ts | 35 ++++++++++++++++++++++++++++ 6 files changed, 148 insertions(+) diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts index 080903374..476c0201a 100644 --- a/deno/lib/ZodError.ts +++ b/deno/lib/ZodError.ts @@ -105,6 +105,7 @@ export type StringValidation = | "ip" | "cidr" | "base64" + | "jwt" | "base64url" | { includes: string; position?: number } | { startsWith: string } diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index d6fec45f3..cb934a97e 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -247,6 +247,44 @@ for (const str of invalidBase64URLStrings) { }); } +test("jwt validations", () => { + const jwt = z.string().jwt(); + const jwtWithAlg = z.string().jwt({ alg: "HS256" }); + + // Valid JWTs + const validHeader = Buffer.from(JSON.stringify({ typ: "JWT", alg: "HS256" })).toString('base64url'); + const validPayload = Buffer.from("{}").toString('base64url'); + const validSignature = "signature"; + const validJWT = `${validHeader}.${validPayload}.${validSignature}`; + + expect(() => jwt.parse(validJWT)).not.toThrow(); + expect(() => jwtWithAlg.parse(validJWT)).not.toThrow(); + + // Invalid format + expect(() => jwt.parse("invalid")).toThrow(); + expect(() => jwt.parse("invalid.invalid")).toThrow(); + expect(() => jwt.parse("invalid.invalid.invalid")).toThrow(); + + // Invalid header + const invalidHeader = Buffer.from("{}").toString('base64url'); + const invalidHeaderJWT = `${invalidHeader}.${validPayload}.${validSignature}`; + expect(() => jwt.parse(invalidHeaderJWT)).toThrow(); + + // Wrong algorithm + const wrongAlgHeader = Buffer.from(JSON.stringify({ typ: "JWT", alg: "RS256" })).toString('base64url'); + const wrongAlgJWT = `${wrongAlgHeader}.${validPayload}.${validSignature}`; + expect(() => jwtWithAlg.parse(wrongAlgJWT)).toThrow(); + + // Custom error message + const customMsg = "Invalid JWT token"; + const jwtWithMsg = z.string().jwt({ message: customMsg }); + try { + jwtWithMsg.parse("invalid"); + } catch (error) { + expect((error as z.ZodError).issues[0].message).toBe(customMsg); + } +}); + test("url validations", () => { const url = z.string().url(); url.parse("http://google.com"); diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 5f366b236..96613e53a 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -604,6 +604,7 @@ export type ZodStringCheck = | { kind: "trim"; message?: string } | { kind: "toLowerCase"; message?: string } | { kind: "toUpperCase"; message?: string } + | { kind: "jwt"; alg?: string; message?: string } | { kind: "datetime"; offset: boolean; @@ -641,6 +642,7 @@ const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/; const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; @@ -739,6 +741,25 @@ function isValidIP(ip: string, version?: IpVersion) { return false; } +function isValidJWT(jwt: string, alg?: string): boolean { + if (!jwtRegex.test(jwt)) return false; + try { + const [header] = jwt.split("."); + // Convert base64url to base64 + const base64 = header + .replace(/-/g, "+") + .replace(/_/g, "/") + .padEnd(header.length + ((4 - (header.length % 4)) % 4), "="); + const decoded = JSON.parse(atob(base64)); + if (typeof decoded !== "object" || decoded === null) return false; + if (!decoded.typ || !decoded.alg) return false; + if (alg && decoded.alg !== alg) return false; + return true; + } catch { + return false; + } +} + function isValidCidr(ip: string, version?: IpVersion) { if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { return true; @@ -1012,6 +1033,16 @@ export class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else if (check.kind === "cidr") { if (!isValidCidr(input.data, check.version)) { ctx = this._getOrReturnCtx(input, ctx); @@ -1105,6 +1136,10 @@ export class ZodString extends ZodType { return this._addCheck({ kind: "base64url", ...errorUtil.errToObj(message) }); } + jwt(options?: { alg?: string; message?: string }) { + return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) }); + } + ip(options?: string | { version?: IpVersion; message?: string }) { return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); } diff --git a/src/ZodError.ts b/src/ZodError.ts index 1511c412a..90a5ce809 100644 --- a/src/ZodError.ts +++ b/src/ZodError.ts @@ -105,6 +105,7 @@ export type StringValidation = | "ip" | "cidr" | "base64" + | "jwt" | "base64url" | { includes: string; position?: number } | { startsWith: string } diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index 4b785c21c..dc01162c4 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -246,6 +246,44 @@ for (const str of invalidBase64URLStrings) { }); } +test("jwt validations", () => { + const jwt = z.string().jwt(); + const jwtWithAlg = z.string().jwt({ alg: "HS256" }); + + // Valid JWTs + const validHeader = Buffer.from(JSON.stringify({ typ: "JWT", alg: "HS256" })).toString('base64url'); + const validPayload = Buffer.from("{}").toString('base64url'); + const validSignature = "signature"; + const validJWT = `${validHeader}.${validPayload}.${validSignature}`; + + expect(() => jwt.parse(validJWT)).not.toThrow(); + expect(() => jwtWithAlg.parse(validJWT)).not.toThrow(); + + // Invalid format + expect(() => jwt.parse("invalid")).toThrow(); + expect(() => jwt.parse("invalid.invalid")).toThrow(); + expect(() => jwt.parse("invalid.invalid.invalid")).toThrow(); + + // Invalid header + const invalidHeader = Buffer.from("{}").toString('base64url'); + const invalidHeaderJWT = `${invalidHeader}.${validPayload}.${validSignature}`; + expect(() => jwt.parse(invalidHeaderJWT)).toThrow(); + + // Wrong algorithm + const wrongAlgHeader = Buffer.from(JSON.stringify({ typ: "JWT", alg: "RS256" })).toString('base64url'); + const wrongAlgJWT = `${wrongAlgHeader}.${validPayload}.${validSignature}`; + expect(() => jwtWithAlg.parse(wrongAlgJWT)).toThrow(); + + // Custom error message + const customMsg = "Invalid JWT token"; + const jwtWithMsg = z.string().jwt({ message: customMsg }); + try { + jwtWithMsg.parse("invalid"); + } catch (error) { + expect((error as z.ZodError).issues[0].message).toBe(customMsg); + } +}); + test("url validations", () => { const url = z.string().url(); url.parse("http://google.com"); diff --git a/src/types.ts b/src/types.ts index 34ee9cb79..850106dd7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -604,6 +604,7 @@ export type ZodStringCheck = | { kind: "trim"; message?: string } | { kind: "toLowerCase"; message?: string } | { kind: "toUpperCase"; message?: string } + | { kind: "jwt"; alg?: string; message?: string } | { kind: "datetime"; offset: boolean; @@ -641,6 +642,7 @@ const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/; const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; @@ -739,6 +741,25 @@ function isValidIP(ip: string, version?: IpVersion) { return false; } +function isValidJWT(jwt: string, alg?: string): boolean { + if (!jwtRegex.test(jwt)) return false; + try { + const [header] = jwt.split("."); + // Convert base64url to base64 + const base64 = header + .replace(/-/g, "+") + .replace(/_/g, "/") + .padEnd(header.length + ((4 - (header.length % 4)) % 4), "="); + const decoded = JSON.parse(atob(base64)); + if (typeof decoded !== "object" || decoded === null) return false; + if (!decoded.typ || !decoded.alg) return false; + if (alg && decoded.alg !== alg) return false; + return true; + } catch { + return false; + } +} + function isValidCidr(ip: string, version?: IpVersion) { if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { return true; @@ -1012,6 +1033,16 @@ export class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else if (check.kind === "cidr") { if (!isValidCidr(input.data, check.version)) { ctx = this._getOrReturnCtx(input, ctx); @@ -1105,6 +1136,10 @@ export class ZodString extends ZodType { return this._addCheck({ kind: "base64url", ...errorUtil.errToObj(message) }); } + jwt(options?: { alg?: string; message?: string }) { + return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) }); + } + ip(options?: string | { version?: IpVersion; message?: string }) { return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); } From 964d622f02408b5e2dc41048557faeb9c3370cee Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Mon, 9 Dec 2024 22:52:52 -0800 Subject: [PATCH 21/30] Fix lint --- deno/lib/README.md | 1 + deno/lib/__tests__/standard-schema.test.ts | 5 ++--- deno/lib/__tests__/string.test.ts | 12 ++++++++---- src/__tests__/standard-schema.test.ts | 5 ++--- src/__tests__/string.test.ts | 12 ++++++++---- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/deno/lib/README.md b/deno/lib/README.md index ce4251d1f..83af7ed6b 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -512,6 +512,7 @@ There are a growing number of tools that are built atop or support Zod natively! - [`@vee-validate/zod`](https://github.com/logaretm/vee-validate/tree/main/packages/zod): Form library for Vue.js with Zod schema validation. - [`zod-form-renderer`](https://github.com/thepeaklab/zod-form-renderer): Auto-infer form fields from zod schema and render them with react-hook-form with E2E type safety. - [`antd-zod`](https://github.com/MrBr/antd-zod): Zod adapter for Ant Design form fields validation. +- [`frrm`](https://github.com/schalkventer/frrm): Tiny 0.5kb Zod-based, HTML form abstraction that goes brr. #### Zod to X diff --git a/deno/lib/__tests__/standard-schema.test.ts b/deno/lib/__tests__/standard-schema.test.ts index 065315c52..11f726ea8 100644 --- a/deno/lib/__tests__/standard-schema.test.ts +++ b/deno/lib/__tests__/standard-schema.test.ts @@ -1,12 +1,11 @@ // @ts-ignore TS6133 import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts"; const test = Deno.test; -import { util } from "../helpers/util.ts"; +import type { StandardSchemaV1 } from "@standard-schema/spec"; +import { util } from "../helpers/util.ts"; import * as z from "../index.ts"; -import type { StandardSchemaV1 } from "@standard-schema/spec"; - test("assignability", () => { const _s1: StandardSchemaV1 = z.string(); const _s2: StandardSchemaV1 = z.string(); diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index cb934a97e..c9aa938af 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -252,8 +252,10 @@ test("jwt validations", () => { const jwtWithAlg = z.string().jwt({ alg: "HS256" }); // Valid JWTs - const validHeader = Buffer.from(JSON.stringify({ typ: "JWT", alg: "HS256" })).toString('base64url'); - const validPayload = Buffer.from("{}").toString('base64url'); + const validHeader = Buffer.from( + JSON.stringify({ typ: "JWT", alg: "HS256" }) + ).toString("base64url"); + const validPayload = Buffer.from("{}").toString("base64url"); const validSignature = "signature"; const validJWT = `${validHeader}.${validPayload}.${validSignature}`; @@ -266,12 +268,14 @@ test("jwt validations", () => { expect(() => jwt.parse("invalid.invalid.invalid")).toThrow(); // Invalid header - const invalidHeader = Buffer.from("{}").toString('base64url'); + const invalidHeader = Buffer.from("{}").toString("base64url"); const invalidHeaderJWT = `${invalidHeader}.${validPayload}.${validSignature}`; expect(() => jwt.parse(invalidHeaderJWT)).toThrow(); // Wrong algorithm - const wrongAlgHeader = Buffer.from(JSON.stringify({ typ: "JWT", alg: "RS256" })).toString('base64url'); + const wrongAlgHeader = Buffer.from( + JSON.stringify({ typ: "JWT", alg: "RS256" }) + ).toString("base64url"); const wrongAlgJWT = `${wrongAlgHeader}.${validPayload}.${validSignature}`; expect(() => jwtWithAlg.parse(wrongAlgJWT)).toThrow(); diff --git a/src/__tests__/standard-schema.test.ts b/src/__tests__/standard-schema.test.ts index 8f67cce76..fbf312f16 100644 --- a/src/__tests__/standard-schema.test.ts +++ b/src/__tests__/standard-schema.test.ts @@ -1,11 +1,10 @@ // @ts-ignore TS6133 import { expect, test } from "@jest/globals"; -import { util } from "../helpers/util"; +import type { StandardSchemaV1 } from "@standard-schema/spec"; +import { util } from "../helpers/util"; import * as z from "../index"; -import type { StandardSchemaV1 } from "@standard-schema/spec"; - test("assignability", () => { const _s1: StandardSchemaV1 = z.string(); const _s2: StandardSchemaV1 = z.string(); diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index dc01162c4..9b18a0241 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -251,8 +251,10 @@ test("jwt validations", () => { const jwtWithAlg = z.string().jwt({ alg: "HS256" }); // Valid JWTs - const validHeader = Buffer.from(JSON.stringify({ typ: "JWT", alg: "HS256" })).toString('base64url'); - const validPayload = Buffer.from("{}").toString('base64url'); + const validHeader = Buffer.from( + JSON.stringify({ typ: "JWT", alg: "HS256" }) + ).toString("base64url"); + const validPayload = Buffer.from("{}").toString("base64url"); const validSignature = "signature"; const validJWT = `${validHeader}.${validPayload}.${validSignature}`; @@ -265,12 +267,14 @@ test("jwt validations", () => { expect(() => jwt.parse("invalid.invalid.invalid")).toThrow(); // Invalid header - const invalidHeader = Buffer.from("{}").toString('base64url'); + const invalidHeader = Buffer.from("{}").toString("base64url"); const invalidHeaderJWT = `${invalidHeader}.${validPayload}.${validSignature}`; expect(() => jwt.parse(invalidHeaderJWT)).toThrow(); // Wrong algorithm - const wrongAlgHeader = Buffer.from(JSON.stringify({ typ: "JWT", alg: "RS256" })).toString('base64url'); + const wrongAlgHeader = Buffer.from( + JSON.stringify({ typ: "JWT", alg: "RS256" }) + ).toString("base64url"); const wrongAlgJWT = `${wrongAlgHeader}.${validPayload}.${validSignature}`; expect(() => jwtWithAlg.parse(wrongAlgJWT)).toThrow(); From b333f96886d01d1f7959aba8a7e4876508b79d79 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Mon, 9 Dec 2024 22:58:35 -0800 Subject: [PATCH 22/30] Fix deno tests --- deno/lib/ZodError.ts | 4 +- deno/lib/__tests__/string.test.ts | 1 + package.json | 240 +++++++++++++++--------------- src/ZodError.ts | 4 +- src/__tests__/string.test.ts | 1 + 5 files changed, 126 insertions(+), 124 deletions(-) diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts index 476c0201a..38af7bf8c 100644 --- a/deno/lib/ZodError.ts +++ b/deno/lib/ZodError.ts @@ -280,10 +280,10 @@ export class ZodError extends Error { } } - toString() { + override toString() { return this.message; } - get message() { + override get message() { return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2); } diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index c9aa938af..0288a23f9 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -1,6 +1,7 @@ // @ts-ignore TS6133 import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts"; const test = Deno.test; +import { Buffer } from "node:buffer"; import * as z from "../index.ts"; diff --git a/package.json b/package.json index d6ecb8918..d3056ab3e 100644 --- a/package.json +++ b/package.json @@ -1,122 +1,122 @@ { - "name": "zod", - "version": "3.23.8", - "author": "Colin McDonnell ", - "repository": { - "type": "git", - "url": "git+https://github.com/colinhacks/zod.git" - }, - "main": "./lib/index.js", - "module": "./lib/index.mjs", - "devDependencies": { - "@babel/core": "^7.22.5", - "@babel/preset-env": "^7.22.5", - "@babel/preset-typescript": "^7.22.5", - "@jest/globals": "^29.4.3", - "@rollup/plugin-typescript": "^8.2.0", - "@standard-schema/spec": "^1.0.0-beta.4", - "@swc/core": "^1.3.66", - "@swc/jest": "^0.2.26", - "@types/benchmark": "^2.1.0", - "@types/jest": "^29.2.2", - "@types/node": "14", - "@typescript-eslint/eslint-plugin": "^5.15.0", - "@typescript-eslint/parser": "^5.15.0", - "babel-jest": "^29.5.0", - "benchmark": "^2.1.4", - "dependency-cruiser": "^9.19.0", - "eslint": "^8.11.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-ban": "^1.6.0", - "eslint-plugin-import": "^2.25.4", - "eslint-plugin-simple-import-sort": "^7.0.0", - "eslint-plugin-unused-imports": "^2.0.0", - "husky": "^7.0.4", - "jest": "^29.3.1", - "lint-staged": "^12.3.7", - "netlify-cli": "^17.26.2", - "nodemon": "^2.0.15", - "prettier": "^2.6.0", - "pretty-quick": "^3.1.3", - "rollup": "^2.70.1", - "ts-jest": "^29.1.0", - "ts-morph": "^14.0.0", - "ts-node": "^10.9.1", - "tslib": "^2.3.1", - "tsx": "^3.8.0", - "typescript": "~4.5.5", - "vitest": "^0.32.2" - }, - "exports": { - ".": { - "types": "./index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.mjs" - }, - "./package.json": "./package.json", - "./locales/*": "./lib/locales/*" - }, - "bugs": { - "url": "https://github.com/colinhacks/zod/issues" - }, - "description": "TypeScript-first schema declaration and validation library with static type inference", - "files": [ - "/lib", - "/index.d.ts" - ], - "funding": "https://github.com/sponsors/colinhacks", - "homepage": "https://zod.dev", - "keywords": [ - "typescript", - "schema", - "validation", - "type", - "inference" - ], - "license": "MIT", - "lint-staged": { - "src/*.ts": [ - "eslint --cache --fix", - "prettier --ignore-unknown --write" - ], - "*.md": [ - "prettier --ignore-unknown --write" - ] - }, - "scripts": { - "prettier:check": "prettier --check src/**/*.ts deno/lib/**/*.ts *.md --no-error-on-unmatched-pattern", - "prettier:fix": "prettier --write src/**/*.ts deno/lib/**/*.ts *.md --ignore-unknown --no-error-on-unmatched-pattern", - "lint:check": "eslint --cache --ext .ts ./src", - "lint:fix": "eslint --cache --fix --ext .ts ./src", - "check": "yarn lint:check && yarn prettier:check", - "fix": "yarn lint:fix && yarn prettier:fix", - "clean": "rm -rf lib/* deno/lib/*", - "build": "yarn run clean && npm run build:cjs && npm run build:esm && npm run build:deno", - "build:deno": "node ./deno-build.mjs && cp ./README.md ./deno/lib", - "build:esm": "rollup --config ./configs/rollup.config.js", - "build:cjs": "tsc -p ./configs/tsconfig.cjs.json", - "build:types": "tsc -p ./configs/tsconfig.types.json", - "build:test": "tsc -p ./configs/tsconfig.test.json", - "test:watch": "yarn test:ts-jest --watch", - "test": "yarn test:ts-jest", - "test:babel": "jest --coverage --config ./configs/babel-jest.config.json", - "test:bun": "bun test src/", - "test:vitest": "npx vitest --config ./configs/vitest.config.ts", - "test:ts-jest": "npx jest --config ./configs/ts-jest.config.json", - "test:swc": "npx jest --config ./configs/swc-jest.config.json", - "test:deno": "cd deno && deno test", - "prepublishOnly": "npm run test && npm run build && npm run build:deno", - "play": "nodemon -e ts -w . -x tsx playground.ts", - "depcruise": "depcruise -c .dependency-cruiser.js src", - "benchmark": "tsx src/benchmarks/index.ts", - "prepare": "husky install", - "docs": "netlify dev" - }, - "sideEffects": false, - "support": { - "backing": { - "npm-funding": true - } - }, - "types": "./index.d.ts" + "name": "zod", + "version": "3.24.0", + "author": "Colin McDonnell ", + "repository": { + "type": "git", + "url": "git+https://github.com/colinhacks/zod.git" + }, + "main": "./lib/index.js", + "module": "./lib/index.mjs", + "devDependencies": { + "@babel/core": "^7.22.5", + "@babel/preset-env": "^7.22.5", + "@babel/preset-typescript": "^7.22.5", + "@jest/globals": "^29.4.3", + "@rollup/plugin-typescript": "^8.2.0", + "@standard-schema/spec": "^1.0.0-beta.4", + "@swc/core": "^1.3.66", + "@swc/jest": "^0.2.26", + "@types/benchmark": "^2.1.0", + "@types/jest": "^29.2.2", + "@types/node": "14", + "@typescript-eslint/eslint-plugin": "^5.15.0", + "@typescript-eslint/parser": "^5.15.0", + "babel-jest": "^29.5.0", + "benchmark": "^2.1.4", + "dependency-cruiser": "^9.19.0", + "eslint": "^8.11.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-ban": "^1.6.0", + "eslint-plugin-import": "^2.25.4", + "eslint-plugin-simple-import-sort": "^7.0.0", + "eslint-plugin-unused-imports": "^2.0.0", + "husky": "^7.0.4", + "jest": "^29.3.1", + "lint-staged": "^12.3.7", + "netlify-cli": "^17.26.2", + "nodemon": "^2.0.15", + "prettier": "^2.6.0", + "pretty-quick": "^3.1.3", + "rollup": "^2.70.1", + "ts-jest": "^29.1.0", + "ts-morph": "^14.0.0", + "ts-node": "^10.9.1", + "tslib": "^2.3.1", + "tsx": "^3.8.0", + "typescript": "~4.5.5", + "vitest": "^0.32.2" + }, + "exports": { + ".": { + "types": "./index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.mjs" + }, + "./package.json": "./package.json", + "./locales/*": "./lib/locales/*" + }, + "bugs": { + "url": "https://github.com/colinhacks/zod/issues" + }, + "description": "TypeScript-first schema declaration and validation library with static type inference", + "files": [ + "/lib", + "/index.d.ts" + ], + "funding": "https://github.com/sponsors/colinhacks", + "homepage": "https://zod.dev", + "keywords": [ + "typescript", + "schema", + "validation", + "type", + "inference" + ], + "license": "MIT", + "lint-staged": { + "src/*.ts": [ + "eslint --cache --fix", + "prettier --ignore-unknown --write" + ], + "*.md": [ + "prettier --ignore-unknown --write" + ] + }, + "scripts": { + "prettier:check": "prettier --check src/**/*.ts deno/lib/**/*.ts *.md --no-error-on-unmatched-pattern", + "prettier:fix": "prettier --write src/**/*.ts deno/lib/**/*.ts *.md --ignore-unknown --no-error-on-unmatched-pattern", + "lint:check": "eslint --cache --ext .ts ./src", + "lint:fix": "eslint --cache --fix --ext .ts ./src", + "check": "yarn lint:check && yarn prettier:check", + "fix": "yarn lint:fix && yarn prettier:fix", + "clean": "rm -rf lib/* deno/lib/*", + "build": "yarn run clean && npm run build:cjs && npm run build:esm && npm run build:deno", + "build:deno": "node ./deno-build.mjs && cp ./README.md ./deno/lib", + "build:esm": "rollup --config ./configs/rollup.config.js", + "build:cjs": "tsc -p ./configs/tsconfig.cjs.json", + "build:types": "tsc -p ./configs/tsconfig.types.json", + "build:test": "tsc -p ./configs/tsconfig.test.json", + "test:watch": "yarn test:ts-jest --watch", + "test": "yarn test:ts-jest", + "test:babel": "jest --coverage --config ./configs/babel-jest.config.json", + "test:bun": "bun test src/", + "test:vitest": "npx vitest --config ./configs/vitest.config.ts", + "test:ts-jest": "npx jest --config ./configs/ts-jest.config.json", + "test:swc": "npx jest --config ./configs/swc-jest.config.json", + "test:deno": "cd deno && deno test", + "prepublishOnly": "npm run test && npm run build && npm run build:deno", + "play": "nodemon -e ts -w . -x tsx playground.ts", + "depcruise": "depcruise -c .dependency-cruiser.js src", + "benchmark": "tsx src/benchmarks/index.ts", + "prepare": "husky install", + "docs": "netlify dev" + }, + "sideEffects": false, + "support": { + "backing": { + "npm-funding": true + } + }, + "types": "./index.d.ts" } diff --git a/src/ZodError.ts b/src/ZodError.ts index 90a5ce809..e10e1622d 100644 --- a/src/ZodError.ts +++ b/src/ZodError.ts @@ -280,10 +280,10 @@ export class ZodError extends Error { } } - toString() { + override toString() { return this.message; } - get message() { + override get message() { return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2); } diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index 9b18a0241..c211c492a 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -1,5 +1,6 @@ // @ts-ignore TS6133 import { expect, test } from "@jest/globals"; +import { Buffer } from "node:buffer"; import * as z from "../index"; From 0c6cbbdd1315683dd3d589fbdc5765c26431dcc9 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Tue, 10 Dec 2024 00:46:06 -0800 Subject: [PATCH 23/30] Undeprecate .nonempty() --- deno/lib/__tests__/string.test.ts | 2 +- deno/lib/types.ts | 8 +++++--- src/__tests__/string.test.ts | 2 +- src/types.ts | 8 +++++--- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index 0288a23f9..bb967bafc 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -8,7 +8,7 @@ import * as z from "../index.ts"; const minFive = z.string().min(5, "min5"); const maxFive = z.string().max(5, "max5"); const justFive = z.string().length(5); -const nonempty = z.string().min(1, "nonempty"); +const nonempty = z.string().nonempty("nonempty"); const includes = z.string().includes("includes"); const includesFromIndex2 = z.string().includes("includes", { position: 2 }); const startsWith = z.string().startsWith("startsWith"); diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 96613e53a..3580f39a1 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -1133,7 +1133,10 @@ export class ZodString extends ZodType { } base64url(message?: errorUtil.ErrMessage) { // base64url encoding is a modification of base64 that can safely be used in URLs and filenames - return this._addCheck({ kind: "base64url", ...errorUtil.errToObj(message) }); + return this._addCheck({ + kind: "base64url", + ...errorUtil.errToObj(message), + }); } jwt(options?: { alg?: string; message?: string }) { @@ -1267,8 +1270,7 @@ export class ZodString extends ZodType { } /** - * @deprecated Use z.string().min(1) instead. - * @see {@link ZodString.min} + * Equivalent to `.min(1)` */ nonempty(message?: errorUtil.ErrMessage) { return this.min(1, errorUtil.errToObj(message)); diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index c211c492a..bc603a933 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -7,7 +7,7 @@ import * as z from "../index"; const minFive = z.string().min(5, "min5"); const maxFive = z.string().max(5, "max5"); const justFive = z.string().length(5); -const nonempty = z.string().min(1, "nonempty"); +const nonempty = z.string().nonempty("nonempty"); const includes = z.string().includes("includes"); const includesFromIndex2 = z.string().includes("includes", { position: 2 }); const startsWith = z.string().startsWith("startsWith"); diff --git a/src/types.ts b/src/types.ts index 850106dd7..b5e76976a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1133,7 +1133,10 @@ export class ZodString extends ZodType { } base64url(message?: errorUtil.ErrMessage) { // base64url encoding is a modification of base64 that can safely be used in URLs and filenames - return this._addCheck({ kind: "base64url", ...errorUtil.errToObj(message) }); + return this._addCheck({ + kind: "base64url", + ...errorUtil.errToObj(message), + }); } jwt(options?: { alg?: string; message?: string }) { @@ -1267,8 +1270,7 @@ export class ZodString extends ZodType { } /** - * @deprecated Use z.string().min(1) instead. - * @see {@link ZodString.min} + * Equivalent to `.min(1)` */ nonempty(message?: errorUtil.ErrMessage) { return this.min(1, errorUtil.errToObj(message)); From 4e219d6ad9d5e56e20afd7423092f506400a29e4 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Tue, 10 Dec 2024 14:57:59 -0800 Subject: [PATCH 24/30] Bump min TS version to 5.0 --- .github/workflows/test.yml | 2 +- deno/lib/types.ts | 3 +-- package.json | 2 +- playground.ts | 13 ------------- src/types.ts | 3 +-- yarn.lock | 5 ----- 6 files changed, 4 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0b1c266b..cb83ff0a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: node: ["latest"] - typescript: ["4.5", "4.6", "4.7", "4.8", "4.9", "5.0", "5.3"] + typescript: ["5.0", "latest"] name: Test with TypeScript ${{ matrix.typescript }} on Node ${{ matrix.node }} steps: - uses: actions/checkout@v4 diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 3580f39a1..cd09d4b15 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -170,8 +170,7 @@ export abstract class ZodType< Output = any, Def extends ZodTypeDef = ZodTypeDef, Input = Output -> implements StandardSchemaV1 -{ +> { readonly _type!: Output; readonly _output!: Output; readonly _input!: Input; diff --git a/package.json b/package.json index d3056ab3e..b783b1d4d 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "ts-node": "^10.9.1", "tslib": "^2.3.1", "tsx": "^3.8.0", - "typescript": "~4.5.5", + "typescript": "^5.0.0", "vitest": "^0.32.2" }, "exports": { diff --git a/playground.ts b/playground.ts index bca434a98..4e01473b6 100644 --- a/playground.ts +++ b/playground.ts @@ -1,16 +1,3 @@ import { z } from "./src"; z; - -const schema = z - .string() - .transform((input) => input || undefined) - .optional() - .default("default"); - -type Input = z.input; // string | undefined -type Output = z.output; // string - -const result = schema.safeParse(""); - -console.log(result); // { success: true, data: undefined } diff --git a/src/types.ts b/src/types.ts index b5e76976a..98281ff2f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -170,8 +170,7 @@ export abstract class ZodType< Output = any, Def extends ZodTypeDef = ZodTypeDef, Input = Output -> implements StandardSchemaV1 -{ +> { readonly _type!: Output; readonly _output!: Output; readonly _input!: Input; diff --git a/yarn.lock b/yarn.lock index 2016a4c07..c3fad795d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11638,11 +11638,6 @@ typescript@^5.0.0, typescript@^5.4.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== -typescript@~4.5.5: - version "4.5.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" - integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== - ufo@^1.3.2: version "1.4.0" resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.4.0.tgz#39845b31be81b4f319ab1d99fd20c56cac528d32" From 65adeeacef0274abbda5438470a3d2bfd376256d Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Tue, 10 Dec 2024 17:38:42 -0800 Subject: [PATCH 25/30] v3.24.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b783b1d4d..3389e63d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zod", - "version": "3.24.0", + "version": "3.24.1", "author": "Colin McDonnell ", "repository": { "type": "git", From cdcf9d4263cc544c7cb49855b31612d4305da72c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:52:51 -0800 Subject: [PATCH 26/30] Bump rollup from 2.79.1 to 2.79.2 (#3895) Bumps [rollup](https://github.com/rollup/rollup) from 2.79.1 to 2.79.2. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v2.79.1...v2.79.2) --- updated-dependencies: - dependency-name: rollup dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index c3fad795d..bb74274cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10388,9 +10388,9 @@ rimraf@^3.0.2: glob "^7.1.3" rollup@^2.70.1: - version "2.79.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" - integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== + version "2.79.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.2.tgz#f150e4a5db4b121a21a747d762f701e5e9f49090" + integrity sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ== optionalDependencies: fsevents "~2.3.2" From a2ad37099e8f7117d231cc2c72d0e471893643b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:52:57 -0800 Subject: [PATCH 27/30] Bump find-my-way from 8.2.0 to 8.2.2 (#3897) Bumps [find-my-way](https://github.com/delvedor/find-my-way) from 8.2.0 to 8.2.2. - [Release notes](https://github.com/delvedor/find-my-way/releases) - [Commits](https://github.com/delvedor/find-my-way/compare/v8.2.0...v8.2.2) --- updated-dependencies: - dependency-name: find-my-way dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index bb74274cf..cfb30ffc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6251,9 +6251,9 @@ finalhandler@1.2.0: unpipe "~1.0.0" find-my-way@^8.0.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-8.2.0.tgz#ef1b83d008114a300118c9c707d8dc65947d9960" - integrity sha512-HdWXgFYc6b1BJcOBDBwjqWuHJj1WYiqrxSh25qtU4DabpMFdj/gSunNBQb83t+8Zt67D7CXEzJWTkxaShMTMOA== + version "8.2.2" + resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-8.2.2.tgz#f3e78bc6ead2da4fdaa201335da3228600ed0285" + integrity sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA== dependencies: fast-deep-equal "^3.1.3" fast-querystring "^1.0.0" From 0e02d66d1bcaad9c0f92609431e1726e088a8112 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:53:09 -0800 Subject: [PATCH 28/30] Bump nanoid from 3.3.7 to 3.3.8 (#3896) Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8. - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8) --- updated-dependencies: - dependency-name: nanoid dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index cfb30ffc6..be4706227 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8763,9 +8763,9 @@ nan@^2.16.0: integrity sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw== nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== napi-build-utils@^1.0.1: version "1.0.2" From 96be65f0d71b0bf8e8f330dc0541cc895edd6459 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:53:19 -0800 Subject: [PATCH 29/30] Bump cross-spawn from 7.0.3 to 7.0.6 (#3882) Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6. - [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md) - [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6) --- updated-dependencies: - dependency-name: cross-spawn dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index be4706227..30a14a0a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4855,9 +4855,9 @@ cron-parser@4.9.0, cron-parser@^4.1.0: luxon "^3.2.1" cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" From f7ad26147ba291cb3fb257545972a8e00e767470 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:53:29 -0800 Subject: [PATCH 30/30] Bump micromatch from 4.0.7 to 4.0.8 (#3748) Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.7 to 4.0.8. - [Release notes](https://github.com/micromatch/micromatch/releases) - [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/micromatch/compare/4.0.7...4.0.8) --- updated-dependencies: - dependency-name: micromatch dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/yarn.lock b/yarn.lock index 30a14a0a6..1ccdec7ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4147,13 +4147,6 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - braces@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" @@ -4161,6 +4154,13 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" +braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + browserslist@^4.22.2, browserslist@^4.22.3: version "4.23.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" @@ -8539,22 +8539,14 @@ micro-memoize@^4.1.2: resolved "https://registry.yarnpkg.com/micro-memoize/-/micro-memoize-4.1.2.tgz#ce719c1ba1e41592f1cd91c64c5f41dcbf135f36" integrity sha512-+HzcV2H+rbSJzApgkj0NdTakkC+bnyeiUxgT6/m7mjcz1CmM22KYFKp+EVj1sWe4UYcnriJr5uqHQD/gMHLD+g== -micromatch@^4.0.2: - version "4.0.7" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" - integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== +micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: braces "^3.0.3" picomatch "^2.3.1" -micromatch@^4.0.4, micromatch@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== - dependencies: - braces "^3.0.2" - picomatch "^2.3.1" - mime-db@1.52.0, mime-db@^1.28.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+

+

+ + + + LibLab + + +
+ Your API Deserves Better SDKs +
+ liblab.com +

+

+

@@ -491,6 +509,7 @@ There are a growing number of tools that are built atop or support Zod natively! - [`sveltekit-superforms`](https://github.com/ciscoheat/sveltekit-superforms): Supercharged form library for SvelteKit with Zod validation. - [`mobx-zod-form`](https://github.com/MonoidDev/mobx-zod-form): Data-first form builder based on MobX & Zod. - [`@vee-validate/zod`](https://github.com/logaretm/vee-validate/tree/main/packages/zod): Form library for Vue.js with Zod schema validation. +- [`zod-form-renderer`](https://github.com/thepeaklab/zod-form-renderer): Auto-infer form fields from zod schema and render them with react-hook-form with E2E type safety. #### Zod to X @@ -505,6 +524,7 @@ There are a growing number of tools that are built atop or support Zod natively! - [`zod-openapi`](https://github.com/samchungy/zod-openapi): Create full OpenAPI v3.x documentation from Zod schemas. - [`fastify-zod-openapi`](https://github.com/samchungy/fastify-zod-openapi): Fastify type provider, validation, serialization and @fastify/swagger support for Zod schemas. - [`typeschema`](https://typeschema.com/): Universal adapter for schema validation. +- [`zodex`](https://github.com/commonbaseapp/zodex): (De)serialization for zod schemas #### X to Zod @@ -538,11 +558,13 @@ There are a growing number of tools that are built atop or support Zod natively! - [`freerstore`](https://github.com/JacobWeisenburger/freerstore): Firestore cost optimizer. - [`slonik`](https://github.com/gajus/slonik/tree/gajus/add-zod-validation-backwards-compatible#runtime-validation-and-static-type-inference): Node.js Postgres client with strong Zod integration. +- [`schemql`](https://github.com/a2lix/schemql): Enhances your SQL workflow by combining raw SQL with targeted type safety and schema validation. - [`soly`](https://github.com/mdbetancourt/soly): Create CLI applications with zod. - [`pastel`](https://github.com/vadimdemedes/pastel): Create CLI applications with react, zod, and ink. - [`zod-xlsx`](https://github.com/sidwebworks/zod-xlsx): A xlsx based resource validator using Zod schemas. - [`znv`](https://github.com/lostfictions/znv): Type-safe environment parsing and validation for Node.js with Zod schemas. - [`zod-config`](https://github.com/alexmarqs/zod-config): Load configurations across multiple sources with flexible adapters, ensuring type safety with Zod. +- [`unplugin-environment`](https://github.com/r17x/js/tree/main/packages/unplugin-environment#readme): A plugin for loading enviroment variables safely with schema validation, simple with virtual module, type-safe with intellisense, and better DX 🔥 🚀 👷. Powered by Zod. #### Utilities for Zod From e04271edf411024dfe9d5f1189f26267947c1e38 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Wed, 6 Nov 2024 19:02:32 -0800 Subject: [PATCH 02/30] Sponsorship tweaks (#3838) * Tweak * Tweak * Tweak * Tweak --- README.md | 12 ++++++------ deno/lib/README.md | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b1246310b..a3c888695 100644 --- a/README.md +++ b/README.md @@ -216,16 +216,16 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod

- - - - LibLab + + + + LibLab
- Your API Deserves Better SDKs + Generate better SDKs for your APIs
- liblab.com + liblab.com

- - - - LibLab + + + + LibLab
- Your API Deserves Better SDKs + Generate better SDKs for your APIs
- liblab.com + liblab.com