Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for base64url strings #3712

Merged
merged 5 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions ERROR_HANDLING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
```

Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,6 @@ bun add zod # bun
pnpm add zod # pnpm
```


> README 的剩余部分假定你是直接通过 npm 安装的`zod`包。
# 基本用法
Expand Down
25 changes: 7 additions & 18 deletions deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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/[email protected]/mod.ts";
```

> The rest of this README assumes you are using npm and importing directly from the `"zod"` package.

## Basic usage
Expand Down
1 change: 1 addition & 0 deletions deno/lib/ZodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export type StringValidation =
| "ip"
| "cidr"
| "base64"
| "base64url"
| { includes: string; position?: number }
| { startsWith: string }
| { endsWith: string };
Expand Down
115 changes: 81 additions & 34 deletions deno/lib/__tests__/string.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
25 changes: 24 additions & 1 deletion deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -969,6 +974,16 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
});
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);
}
Expand Down Expand Up @@ -1027,6 +1042,10 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
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) });
Expand Down Expand Up @@ -1235,6 +1254,10 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
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;
Expand Down
1 change: 1 addition & 0 deletions src/ZodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export type StringValidation =
| "ip"
| "cidr"
| "base64"
| "base64url"
| { includes: string; position?: number }
| { startsWith: string }
| { endsWith: string };
Expand Down
Loading
Loading