Skip to content

Commit 69a1798

Browse files
authored
Implement Standard Schema spec (#3850)
* Implement Standard Schema * Remove dep * WIP * Fix CI * Update to latest standard-schema * Add standard-schema/spec as devDep
1 parent 963386d commit 69a1798

File tree

9 files changed

+560
-6
lines changed

9 files changed

+560
-6
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// @ts-ignore TS6133
2+
import { expect } from "https://deno.land/x/[email protected]/mod.ts";
3+
const test = Deno.test;
4+
import { util } from "../helpers/util.ts";
5+
6+
import * as z from "../index.ts";
7+
8+
import type { StandardSchemaV1 } from "@standard-schema/spec";
9+
10+
test("assignability", () => {
11+
const _s1: StandardSchemaV1 = z.string();
12+
const _s2: StandardSchemaV1<string> = z.string();
13+
const _s3: StandardSchemaV1<string, string> = z.string();
14+
const _s4: StandardSchemaV1<unknown, string> = z.string();
15+
[_s1, _s2, _s3, _s4];
16+
});
17+
18+
test("type inference", () => {
19+
const stringToNumber = z.string().transform((x) => x.length);
20+
type input = StandardSchemaV1.InferInput<typeof stringToNumber>;
21+
util.assertEqual<input, string>(true);
22+
type output = StandardSchemaV1.InferOutput<typeof stringToNumber>;
23+
util.assertEqual<output, number>(true);
24+
});
25+
26+
test("valid parse", () => {
27+
const schema = z.string();
28+
const result = schema["~standard"]["validate"]("hello");
29+
if (result instanceof Promise) {
30+
throw new Error("Expected sync result");
31+
}
32+
expect(result.issues).toEqual(undefined);
33+
if (result.issues) {
34+
throw new Error("Expected no issues");
35+
} else {
36+
expect(result.value).toEqual("hello");
37+
}
38+
});
39+
40+
test("invalid parse", () => {
41+
const schema = z.string();
42+
const result = schema["~standard"]["validate"](1234);
43+
if (result instanceof Promise) {
44+
throw new Error("Expected sync result");
45+
}
46+
expect(result.issues).toBeDefined();
47+
if (!result.issues) {
48+
throw new Error("Expected issues");
49+
}
50+
expect(result.issues.length).toEqual(1);
51+
expect(result.issues[0].path).toEqual([]);
52+
});
53+
54+
test("valid parse async", async () => {
55+
const schema = z.string().refine(async () => true);
56+
const _result = schema["~standard"]["validate"]("hello");
57+
if (_result instanceof Promise) {
58+
const result = await _result;
59+
expect(result.issues).toEqual(undefined);
60+
if (result.issues) {
61+
throw new Error("Expected no issues");
62+
} else {
63+
expect(result.value).toEqual("hello");
64+
}
65+
} else {
66+
throw new Error("Expected async result");
67+
}
68+
});
69+
70+
test("invalid parse async", async () => {
71+
const schema = z.string().refine(async () => false);
72+
const _result = schema["~standard"]["validate"]("hello");
73+
if (_result instanceof Promise) {
74+
const result = await _result;
75+
expect(result.issues).toBeDefined();
76+
if (!result.issues) {
77+
throw new Error("Expected issues");
78+
}
79+
expect(result.issues.length).toEqual(1);
80+
expect(result.issues[0].path).toEqual([]);
81+
} else {
82+
throw new Error("Expected async result");
83+
}
84+
});

deno/lib/standard-schema.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* The Standard Schema interface.
3+
*/
4+
export type StandardSchemaV1<Input = unknown, Output = Input> = {
5+
/**
6+
* The Standard Schema properties.
7+
*/
8+
readonly "~standard": StandardSchemaV1.Props<Input, Output>;
9+
};
10+
11+
export declare namespace StandardSchemaV1 {
12+
/**
13+
* The Standard Schema properties interface.
14+
*/
15+
export interface Props<Input = unknown, Output = Input> {
16+
/**
17+
* The version number of the standard.
18+
*/
19+
readonly version: 1;
20+
/**
21+
* The vendor name of the schema library.
22+
*/
23+
readonly vendor: string;
24+
/**
25+
* Validates unknown input values.
26+
*/
27+
readonly validate: (
28+
value: unknown
29+
) => Result<Output> | Promise<Result<Output>>;
30+
/**
31+
* Inferred types associated with the schema.
32+
*/
33+
readonly types?: Types<Input, Output> | undefined;
34+
}
35+
36+
/**
37+
* The result interface of the validate function.
38+
*/
39+
export type Result<Output> = SuccessResult<Output> | FailureResult;
40+
41+
/**
42+
* The result interface if validation succeeds.
43+
*/
44+
export interface SuccessResult<Output> {
45+
/**
46+
* The typed output value.
47+
*/
48+
readonly value: Output;
49+
/**
50+
* The non-existent issues.
51+
*/
52+
readonly issues?: undefined;
53+
}
54+
55+
/**
56+
* The result interface if validation fails.
57+
*/
58+
export interface FailureResult {
59+
/**
60+
* The issues of failed validation.
61+
*/
62+
readonly issues: ReadonlyArray<Issue>;
63+
}
64+
65+
/**
66+
* The issue interface of the failure output.
67+
*/
68+
export interface Issue {
69+
/**
70+
* The error message of the issue.
71+
*/
72+
readonly message: string;
73+
/**
74+
* The path of the issue, if any.
75+
*/
76+
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
77+
}
78+
79+
/**
80+
* The path segment interface of the issue.
81+
*/
82+
export interface PathSegment {
83+
/**
84+
* The key representing a path segment.
85+
*/
86+
readonly key: PropertyKey;
87+
}
88+
89+
/**
90+
* The Standard Schema types interface.
91+
*/
92+
export interface Types<Input = unknown, Output = Input> {
93+
/**
94+
* The input type of the schema.
95+
*/
96+
readonly input: Input;
97+
/**
98+
* The output type of the schema.
99+
*/
100+
readonly output: Output;
101+
}
102+
103+
/**
104+
* Infers the input type of a Standard Schema.
105+
*/
106+
export type InferInput<Schema extends StandardSchemaV1> = NonNullable<
107+
Schema["~standard"]["types"]
108+
>["input"];
109+
110+
/**
111+
* Infers the output type of a Standard Schema.
112+
*/
113+
export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
114+
Schema["~standard"]["types"]
115+
>["output"];
116+
117+
// biome-ignore lint/complexity/noUselessEmptyExport: needed for granular visibility control of TS namespace
118+
export {};
119+
}

deno/lib/types.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { partialUtil } from "./helpers/partialUtil.ts";
2424
import { Primitive } from "./helpers/typeAliases.ts";
2525
import { getParsedType, objectUtil, util, ZodParsedType } from "./helpers/util.ts";
26+
import type { StandardSchemaV1 } from "./standard-schema.ts";
2627
import {
2728
IssueData,
2829
StringValidation,
@@ -169,7 +170,8 @@ export abstract class ZodType<
169170
Output = any,
170171
Def extends ZodTypeDef = ZodTypeDef,
171172
Input = Output
172-
> {
173+
> implements StandardSchemaV1<Input, Output>
174+
{
173175
readonly _type!: Output;
174176
readonly _output!: Output;
175177
readonly _input!: Input;
@@ -179,6 +181,8 @@ export abstract class ZodType<
179181
return this._def.description;
180182
}
181183

184+
"~standard": StandardSchemaV1.Props<Input, Output>;
185+
182186
abstract _parse(input: ParseInput): ParseReturnType<Output>;
183187

184188
_getType(input: ParseInput): string {
@@ -262,6 +266,55 @@ export abstract class ZodType<
262266
return handleResult(ctx, result);
263267
}
264268

269+
"~validate"(
270+
data: unknown
271+
):
272+
| StandardSchemaV1.Result<Output>
273+
| Promise<StandardSchemaV1.Result<Output>> {
274+
const ctx: ParseContext = {
275+
common: {
276+
issues: [],
277+
async: !!(this["~standard"] as any).async,
278+
},
279+
path: [],
280+
schemaErrorMap: this._def.errorMap,
281+
parent: null,
282+
data,
283+
parsedType: getParsedType(data),
284+
};
285+
286+
if (!(this["~standard"] as any).async) {
287+
try {
288+
const result = this._parseSync({ data, path: [], parent: ctx });
289+
return isValid(result)
290+
? {
291+
value: result.value,
292+
}
293+
: {
294+
issues: ctx.common.issues,
295+
};
296+
} catch (err: any) {
297+
if ((err as Error)?.message?.toLowerCase()?.includes("encountered")) {
298+
(this["~standard"] as any).async = true;
299+
}
300+
(ctx as any).common = {
301+
issues: [],
302+
async: true,
303+
};
304+
}
305+
}
306+
307+
return this._parseAsync({ data, path: [], parent: ctx }).then((result) =>
308+
isValid(result)
309+
? {
310+
value: result.value,
311+
}
312+
: {
313+
issues: ctx.common.issues,
314+
}
315+
);
316+
}
317+
265318
async parseAsync(
266319
data: unknown,
267320
params?: Partial<ParseParams>
@@ -422,6 +475,11 @@ export abstract class ZodType<
422475
this.readonly = this.readonly.bind(this);
423476
this.isNullable = this.isNullable.bind(this);
424477
this.isOptional = this.isOptional.bind(this);
478+
this["~standard"] = {
479+
version: 1,
480+
vendor: "zod",
481+
validate: (data) => this["~validate"](data),
482+
};
425483
}
426484

427485
optional(): ZodOptional<this> {

package.json

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@babel/preset-typescript": "^7.22.5",
1515
"@jest/globals": "^29.4.3",
1616
"@rollup/plugin-typescript": "^8.2.0",
17+
"@standard-schema/spec": "^1.0.0-beta.4",
1718
"@swc/core": "^1.3.66",
1819
"@swc/jest": "^0.2.26",
1920
"@types/benchmark": "^2.1.0",
@@ -59,14 +60,28 @@
5960
"url": "https://github.com/colinhacks/zod/issues"
6061
},
6162
"description": "TypeScript-first schema declaration and validation library with static type inference",
62-
"files": ["/lib", "/index.d.ts"],
63+
"files": [
64+
"/lib",
65+
"/index.d.ts"
66+
],
6367
"funding": "https://github.com/sponsors/colinhacks",
6468
"homepage": "https://zod.dev",
65-
"keywords": ["typescript", "schema", "validation", "type", "inference"],
69+
"keywords": [
70+
"typescript",
71+
"schema",
72+
"validation",
73+
"type",
74+
"inference"
75+
],
6676
"license": "MIT",
6777
"lint-staged": {
68-
"src/*.ts": ["eslint --cache --fix", "prettier --ignore-unknown --write"],
69-
"*.md": ["prettier --ignore-unknown --write"]
78+
"src/*.ts": [
79+
"eslint --cache --fix",
80+
"prettier --ignore-unknown --write"
81+
],
82+
"*.md": [
83+
"prettier --ignore-unknown --write"
84+
]
7085
},
7186
"scripts": {
7287
"prettier:check": "prettier --check src/**/*.ts deno/lib/**/*.ts *.md --no-error-on-unmatched-pattern",

playground.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
11
import { z } from "./src";
22

33
z;
4+
5+
const schema = z
6+
.string()
7+
.transform((input) => input || undefined)
8+
.optional()
9+
.default("default");
10+
11+
type Input = z.input<typeof schema>; // string | undefined
12+
type Output = z.output<typeof schema>; // string
13+
14+
const result = schema.safeParse("");
15+
16+
console.log(result); // { success: true, data: undefined }

0 commit comments

Comments
 (0)