zod
Version:
TypeScript-first schema declaration and validation library with static type inference
1,917 lines (1,808 loc) • 59.3 kB
text/typescript
import { describe, expect, test } from "vitest";
import * as z from "zod/v4";
// import * as zCore from "zod/v4/core";
describe("toJSONSchema", () => {
test("primitive types", () => {
expect(z.toJSONSchema(z.string())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
}
`);
expect(z.toJSONSchema(z.number())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "number",
}
`);
expect(z.toJSONSchema(z.boolean())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "boolean",
}
`);
expect(z.toJSONSchema(z.null())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "null",
}
`);
expect(z.toJSONSchema(z.undefined(), { unrepresentable: "any" })).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
}
`);
expect(z.toJSONSchema(z.any())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
}
`);
expect(z.toJSONSchema(z.unknown())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
}
`);
expect(z.toJSONSchema(z.never())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"not": {},
}
`);
expect(z.toJSONSchema(z.email())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.iso.datetime())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.iso.date())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "date",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.iso.time())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "time",
"pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.iso.duration())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "duration",
"pattern": "^P(?:(\\d+W)|(?!.*W)(?=\\d|T\\d)(\\d+Y)?(\\d+M)?(\\d+D)?(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+([.,]\\d+)?S)?)?)$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.ipv4())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "ipv4",
"pattern": "^(?:(?: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])$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.ipv6())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "ipv6",
"pattern": "^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.uuid())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.guid())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.url())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "uri",
"type": "string",
}
`);
expect(z.toJSONSchema(z.base64())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contentEncoding": "base64",
"format": "base64",
"pattern": "^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.cuid())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "cuid",
"pattern": "^[cC][^\\s-]{8,}$",
"type": "string",
}
`);
// expect(z.toJSONSchema(z.regex(/asdf/))).toMatchInlineSnapshot();
expect(z.toJSONSchema(z.emoji())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "emoji",
"pattern": "^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.nanoid())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "nanoid",
"pattern": "^[a-zA-Z0-9_-]{21}$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.cuid2())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "cuid2",
"pattern": "^[0-9a-z]+$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.ulid())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "ulid",
"pattern": "^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$",
"type": "string",
}
`);
// expect(z.toJSONSchema(z.cidr())).toMatchInlineSnapshot();
expect(z.toJSONSchema(z.number())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "number",
}
`);
expect(z.toJSONSchema(z.int())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"type": "integer",
}
`);
expect(z.toJSONSchema(z.int32())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maximum": 2147483647,
"minimum": -2147483648,
"type": "integer",
}
`);
expect(z.toJSONSchema(z.float32())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maximum": 3.4028234663852886e+38,
"minimum": -3.4028234663852886e+38,
"type": "number",
}
`);
expect(z.toJSONSchema(z.float64())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maximum": 1.7976931348623157e+308,
"minimum": -1.7976931348623157e+308,
"type": "number",
}
`);
expect(z.toJSONSchema(z.jwt())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "jwt",
"type": "string",
}
`);
});
test("unsupported schema types", () => {
expect(() => z.toJSONSchema(z.bigint())).toThrow("BigInt cannot be represented in JSON Schema");
expect(() => z.toJSONSchema(z.int64())).toThrow("BigInt cannot be represented in JSON Schema");
expect(() => z.toJSONSchema(z.symbol())).toThrow("Symbols cannot be represented in JSON Schema");
expect(() => z.toJSONSchema(z.void())).toThrow("Void cannot be represented in JSON Schema");
expect(() => z.toJSONSchema(z.undefined())).toThrow("Undefined cannot be represented in JSON Schema");
expect(() => z.toJSONSchema(z.date())).toThrow("Date cannot be represented in JSON Schema");
expect(() => z.toJSONSchema(z.map(z.string(), z.number()))).toThrow("Map cannot be represented in JSON Schema");
expect(() => z.toJSONSchema(z.set(z.string()))).toThrow("Set cannot be represented in JSON Schema");
expect(() => z.toJSONSchema(z.custom(() => true))).toThrow("Custom types cannot be represented in JSON Schema");
// Transform
const transformSchema = z.string().transform((val) => Number.parseInt(val));
expect(() => z.toJSONSchema(transformSchema)).toThrow("Transforms cannot be represented in JSON Schema");
// Static catch values
const staticCatchSchema = z.string().catch(() => "sup");
expect(z.toJSONSchema(staticCatchSchema)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"default": "sup",
"type": "string",
}
`);
// Dynamic catch values
const dynamicCatchSchema = z.string().catch((ctx) => `${ctx.issues.length}`);
expect(() => z.toJSONSchema(dynamicCatchSchema)).toThrow("Dynamic catch values are not supported in JSON Schema");
});
test("string formats", () => {
expect(z.toJSONSchema(z.string().email())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.string().uuid())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.iso.datetime())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.iso.date())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "date",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.iso.time())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "time",
"pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.iso.duration())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "duration",
"pattern": "^P(?:(\\d+W)|(?!.*W)(?=\\d|T\\d)(\\d+Y)?(\\d+M)?(\\d+D)?(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+([.,]\\d+)?S)?)?)$",
"type": "string",
}
`);
// expect(z.toJSONSchema(z.string().ip())).toMatchInlineSnapshot(`
// {
// "pattern": /\\(\\^\\(\\?:\\(\\?: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\\]\\)\\$\\)\\|\\(\\^\\(\\(\\[a-fA-F0-9\\]\\{1,4\\}:\\)\\{7\\}\\|::\\(\\[a-fA-F0-9\\]\\{1,4\\}:\\)\\{0,6\\}\\|\\(\\[a-fA-F0-9\\]\\{1,4\\}:\\)\\{1\\}:\\(\\[a-fA-F0-9\\]\\{1,4\\}:\\)\\{0,5\\}\\|\\(\\[a-fA-F0-9\\]\\{1,4\\}:\\)\\{2\\}:\\(\\[a-fA-F0-9\\]\\{1,4\\}:\\)\\{0,4\\}\\|\\(\\[a-fA-F0-9\\]\\{1,4\\}:\\)\\{3\\}:\\(\\[a-fA-F0-9\\]\\{1,4\\}:\\)\\{0,3\\}\\|\\(\\[a-fA-F0-9\\]\\{1,4\\}:\\)\\{4\\}:\\(\\[a-fA-F0-9\\]\\{1,4\\}:\\)\\{0,2\\}\\|\\(\\[a-fA-F0-9\\]\\{1,4\\}:\\)\\{5\\}:\\(\\[a-fA-F0-9\\]\\{1,4\\}:\\)\\{0,1\\}\\)\\(\\[a-fA-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\\}\\)\\)\\)\\$\\)/,
// "type": "string",
// }
// `);
expect(z.toJSONSchema(z.ipv4())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "ipv4",
"pattern": "^(?:(?: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])$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.ipv6())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "ipv6",
"pattern": "^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.base64())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contentEncoding": "base64",
"format": "base64",
"pattern": "^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.url())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "uri",
"type": "string",
}
`);
expect(z.toJSONSchema(z.guid())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.string().regex(/asdf/))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"pattern": "asdf",
"type": "string",
}
`);
});
test("string patterns", () => {
expect(
z.toJSONSchema(
z
.string()
.startsWith("hello")
.includes("cruel")
.includes("dark", { position: 10 })
.endsWith("world")
.regex(/stuff/)
)
).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"pattern": "^hello.*",
},
{
"pattern": "cruel",
},
{
"pattern": "^.{10}dark",
},
{
"pattern": ".*world$",
},
{
"pattern": "stuff",
},
],
"type": "string",
}
`);
expect(
z.toJSONSchema(
z
.string()
.startsWith("hello")
.includes("cruel")
.includes("dark", { position: 10 })
.endsWith("world")
.regex(/stuff/),
{
target: "draft-7",
}
)
).toMatchInlineSnapshot(`
{
"$schema": "http://json-schema.org/draft-07/schema#",
"allOf": [
{
"pattern": "^hello.*",
"type": "string",
},
{
"pattern": "cruel",
"type": "string",
},
{
"pattern": "^.{10}dark",
"type": "string",
},
{
"pattern": ".*world$",
"type": "string",
},
{
"pattern": "stuff",
"type": "string",
},
],
"type": "string",
}
`);
});
test("number constraints", () => {
expect(z.toJSONSchema(z.number().min(5).max(10))).toMatchInlineSnapshot(
`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maximum": 10,
"minimum": 5,
"type": "number",
}
`
);
expect(z.toJSONSchema(z.number().gt(5).gt(10))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"exclusiveMinimum": 10,
"type": "number",
}
`);
expect(z.toJSONSchema(z.number().gt(5).gte(10))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minimum": 10,
"type": "number",
}
`);
expect(z.toJSONSchema(z.number().lt(5).lt(3))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"exclusiveMaximum": 3,
"type": "number",
}
`);
expect(z.toJSONSchema(z.number().lt(5).lt(3).lte(2))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maximum": 2,
"type": "number",
}
`);
expect(z.toJSONSchema(z.number().lt(5).lte(3))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maximum": 3,
"type": "number",
}
`);
expect(z.toJSONSchema(z.number().gt(5).lt(10))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"exclusiveMaximum": 10,
"exclusiveMinimum": 5,
"type": "number",
}
`);
expect(z.toJSONSchema(z.number().gte(5).lte(10))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maximum": 10,
"minimum": 5,
"type": "number",
}
`);
expect(z.toJSONSchema(z.number().positive())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"exclusiveMinimum": 0,
"type": "number",
}
`);
expect(z.toJSONSchema(z.number().negative())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"exclusiveMaximum": 0,
"type": "number",
}
`);
expect(z.toJSONSchema(z.number().nonpositive())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maximum": 0,
"type": "number",
}
`);
expect(z.toJSONSchema(z.number().nonnegative())).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minimum": 0,
"type": "number",
}
`);
});
test("arrays", () => {
expect(z.toJSONSchema(z.array(z.string()))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": {
"type": "string",
},
"type": "array",
}
`);
});
test("unions", () => {
const schema = z.union([z.string(), z.number()]);
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
{
"type": "string",
},
{
"type": "number",
},
],
}
`);
});
test("intersections", () => {
const schema = z.intersection(z.object({ name: z.string() }), z.object({ age: z.number() }));
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"age": {
"type": "number",
},
},
"required": [
"age",
],
"type": "object",
},
],
}
`);
});
test("record", () => {
const schema = z.record(z.string(), z.boolean());
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": {
"type": "boolean",
},
"propertyNames": {
"type": "string",
},
"type": "object",
}
`);
});
test("tuple", () => {
const schema = z.tuple([z.string(), z.number()]).rest(z.boolean());
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": {
"type": "boolean",
},
"prefixItems": [
{
"type": "string",
},
{
"type": "number",
},
],
"type": "array",
}
`);
});
test("promise", () => {
const schema = z.promise(z.string());
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
}
`);
});
test("lazy", () => {
const schema = z.lazy(() => z.string());
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
}
`);
});
// enum
test("enum", () => {
const a = z.enum(["a", "b", "c"]);
expect(z.toJSONSchema(a)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
"a",
"b",
"c",
],
"type": "string",
}
`);
enum B {
A = 0,
B = 1,
C = 2,
}
const b = z.enum(B);
expect(z.toJSONSchema(b)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
0,
1,
2,
],
"type": "number",
}
`);
});
// literal
test("literal", () => {
const a = z.literal("hello");
expect(z.toJSONSchema(a)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": "hello",
"type": "string",
}
`);
const b = z.literal(7);
expect(z.toJSONSchema(b)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": 7,
"type": "number",
}
`);
const c = z.literal(["hello", undefined, null, 5, BigInt(1324)]);
expect(() => z.toJSONSchema(c)).toThrow();
const d = z.literal(["hello", null, 5]);
expect(z.toJSONSchema(d)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
"hello",
null,
5,
],
}
`);
const e = z.literal(["hello", "zod", "v4"]);
expect(z.toJSONSchema(e)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
"hello",
"zod",
"v4",
],
"type": "string",
}
`);
});
// pipe
test("pipe", () => {
const schema = z
.string()
.transform((val) => Number.parseInt(val))
.pipe(z.number());
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "number",
}
`);
});
test("simple objects", () => {
const schema = z.object({
name: z.string(),
age: z.number(),
});
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(
`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"age": {
"type": "number",
},
"name": {
"type": "string",
},
},
"required": [
"name",
"age",
],
"type": "object",
}
`
);
});
test("additionalproperties in z.object", () => {
const a = z.object({
name: z.string(),
});
expect(z.toJSONSchema(a)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
}
`);
expect(z.toJSONSchema(a, { io: "input" })).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"name": {
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
}
`);
expect(
z.toJSONSchema(a, {
io: "input",
override(ctx) {
const def = ctx.zodSchema._zod.def;
if (def.type === "object" && !def.catchall) {
(ctx.jsonSchema as z.core.JSONSchema.ObjectSchema).additionalProperties = false;
}
},
})
).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
}
`);
});
test("catchall objects", () => {
const a = z.strictObject({
name: z.string(),
age: z.number(),
});
expect(z.toJSONSchema(a)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"age": {
"type": "number",
},
"name": {
"type": "string",
},
},
"required": [
"name",
"age",
],
"type": "object",
}
`);
const b = z
.object({
name: z.string(),
})
.catchall(z.string());
expect(z.toJSONSchema(b)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": {
"type": "string",
},
"properties": {
"name": {
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
}
`);
const c = z.looseObject({
name: z.string(),
});
expect(z.toJSONSchema(c)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": {},
"properties": {
"name": {
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
}
`);
});
test("optional fields - object", () => {
const schema = z.object({
required: z.string(),
optional: z.string().optional(),
nonoptional: z.string().optional().nonoptional(),
});
const result = z.toJSONSchema(schema);
expect(result).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"nonoptional": {
"type": "string",
},
"optional": {
"type": "string",
},
"required": {
"type": "string",
},
},
"required": [
"required",
"nonoptional",
],
"type": "object",
}
`);
});
test("recursive object", () => {
interface Category {
name: string;
subcategories: Category[];
}
const categorySchema: z.ZodType<Category> = z.object({
name: z.string(),
subcategories: z.array(z.lazy(() => categorySchema)),
});
const result = z.toJSONSchema(categorySchema);
expect(result).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
},
"subcategories": {
"items": {
"$ref": "#",
},
"type": "array",
},
},
"required": [
"name",
"subcategories",
],
"type": "object",
}
`);
});
test("simple interface", () => {
const userSchema = z.object({
name: z.string(),
age: z.number().optional(),
});
const result = z.toJSONSchema(userSchema);
expect(result).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"age": {
"type": "number",
},
"name": {
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
}
`);
});
test("catchall interface", () => {
const a = z.strictObject({
name: z.string(),
age: z.number(),
});
expect(z.toJSONSchema(a)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"age": {
"type": "number",
},
"name": {
"type": "string",
},
},
"required": [
"name",
"age",
],
"type": "object",
}
`);
const b = z
.object({
name: z.string(),
})
.catchall(z.string());
expect(z.toJSONSchema(b)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": {
"type": "string",
},
"properties": {
"name": {
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
}
`);
const c = z.looseObject({
name: z.string(),
});
expect(z.toJSONSchema(c)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": {},
"properties": {
"name": {
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
}
`);
});
test("recursive interface schemas", () => {
const TreeNodeSchema = z.object({
id: z.string(),
get children() {
return TreeNodeSchema;
},
});
const result = z.toJSONSchema(TreeNodeSchema);
// Should have definitions for recursive schema
expect(JSON.stringify(result, null, 2)).toMatchInlineSnapshot(
`
"{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string"
},
"children": {
"$ref": "#"
}
},
"required": [
"id",
"children"
],
"additionalProperties": false
}"
`
);
});
test("mutually recursive interface schemas", () => {
const FolderSchema = z.object({
name: z.string(),
get files() {
return z.array(FileSchema);
},
});
const FileSchema = z.object({
name: z.string(),
get parent() {
return FolderSchema;
},
});
const result = z.toJSONSchema(FolderSchema);
// Should have definitions for both schemas
expect(JSON.stringify(result, null, 2)).toMatchInlineSnapshot(
`
"{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"files": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"parent": {
"$ref": "#"
}
},
"required": [
"name",
"parent"
],
"additionalProperties": false
}
}
},
"required": [
"name",
"files"
],
"additionalProperties": false
}"
`
);
});
});
test("override", () => {
const schema = z.z.toJSONSchema(z.string(), {
override: (ctx) => {
ctx.zodSchema;
ctx.jsonSchema;
ctx.jsonSchema.whatever = "sup";
},
});
expect(schema).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
"whatever": "sup",
}
`);
});
test("override: do not run on references", () => {
let overrideCount = 0;
const schema = z
.union([z.string().date(), z.string().datetime(), z.string().datetime({ local: true })])
.meta({ a: true })
.transform((str) => new Date(str))
.meta({ b: true })
.pipe(z.date())
.meta({ c: true })
.brand("dateIn");
z.z.toJSONSchema(schema, {
unrepresentable: "any",
io: "input",
override(_) {
overrideCount++;
},
});
expect(overrideCount).toBe(6);
});
test("override with refs", () => {
const a = z.string().optional();
const result = z.z.toJSONSchema(a, {
override(ctx) {
if (ctx.zodSchema._zod.def.type === "string") {
ctx.jsonSchema.type = "STRING" as "string";
}
},
});
expect(result).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "STRING",
}
`);
});
test("override execution order", () => {
const schema = z.union([z.string(), z.number()]);
let unionSchema!: any;
z.z.toJSONSchema(schema, {
override(ctx) {
if (ctx.zodSchema._zod.def.type === "union") {
unionSchema = ctx.jsonSchema;
}
},
});
expect(unionSchema).toMatchInlineSnapshot(`
{
"anyOf": [
{
"type": "string",
},
{
"type": "number",
},
],
}
`);
});
test("override with path", () => {
const userSchema = z.object({
name: z.string(),
age: z.number(),
});
const capturedPaths: (string | number)[][] = [];
z.toJSONSchema(userSchema, {
override(ctx) {
capturedPaths.push(ctx.path);
},
});
expect(capturedPaths).toMatchInlineSnapshot(`
[
[
"properties",
"age",
],
[
"properties",
"name",
],
[],
]
`);
});
test("pipe", () => {
const mySchema = z
.string()
.transform((val) => val.length)
.pipe(z.number());
// ZodPipe
const a = z.z.toJSONSchema(mySchema);
expect(a).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "number",
}
`);
// => { type: "number" }
const b = z.z.toJSONSchema(mySchema, { io: "input" });
expect(b).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
}
`);
// => { type: "string" }
});
test("passthrough schemas", () => {
const Internal = z.object({
num: z.number(),
str: z.string(),
});
//.meta({ id: "Internal" });
const External = z.object({
a: Internal,
b: Internal.optional(),
c: z.lazy(() => Internal),
d: z.promise(Internal),
e: z.pipe(Internal, Internal),
});
const result = z.z.toJSONSchema(External, {
reused: "ref",
});
expect(result).toMatchInlineSnapshot(`
{
"$defs": {
"__schema0": {
"additionalProperties": false,
"properties": {
"num": {
"type": "number",
},
"str": {
"type": "string",
},
},
"required": [
"num",
"str",
],
"type": "object",
},
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"a": {
"$ref": "#/$defs/__schema0",
},
"b": {
"$ref": "#/$defs/__schema0",
},
"c": {
"$ref": "#/$defs/__schema0",
},
"d": {
"$ref": "#/$defs/__schema0",
},
"e": {
"$ref": "#/$defs/__schema0",
},
},
"required": [
"a",
"c",
"d",
"e",
],
"type": "object",
}
`);
});
test("extract schemas with id", () => {
const name = z.string().meta({ id: "name" });
const result = z.z.toJSONSchema(
z.object({
first_name: name,
last_name: name.nullable(),
middle_name: name.optional(),
age: z.number().meta({ id: "age" }),
})
);
expect(result).toMatchInlineSnapshot(`
{
"$defs": {
"age": {
"id": "age",
"type": "number",
},
"name": {
"id": "name",
"type": "string",
},
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"age": {
"$ref": "#/$defs/age",
},
"first_name": {
"$ref": "#/$defs/name",
},
"last_name": {
"anyOf": [
{
"$ref": "#/$defs/name",
},
{
"type": "null",
},
],
},
"middle_name": {
"$ref": "#/$defs/name",
},
},
"required": [
"first_name",
"last_name",
"age",
],
"type": "object",
}
`);
});
test("unrepresentable literal values are ignored", () => {
const a = z.z.toJSONSchema(z.literal(["hello", null, 5, BigInt(1324), undefined]), { unrepresentable: "any" });
expect(a).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
"hello",
null,
5,
1324,
],
}
`);
const b = z.z.toJSONSchema(z.literal([undefined, null, 5, BigInt(1324)]), { unrepresentable: "any" });
expect(b).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
null,
5,
1324,
],
}
`);
const c = z.z.toJSONSchema(z.literal([undefined]), { unrepresentable: "any" });
expect(c).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
}
`);
});
test("describe with id", () => {
const jobId = z.string().meta({ id: "jobId" });
const a = z.z.toJSONSchema(
z.object({
current: jobId.describe("Current job"),
previous: jobId.describe("Previous job"),
})
);
expect(a).toMatchInlineSnapshot(`
{
"$defs": {
"jobId": {
"id": "jobId",
"type": "string",
},
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"current": {
"$ref": "#/$defs/jobId",
"description": "Current job",
},
"previous": {
"$ref": "#/$defs/jobId",
"description": "Previous job",
},
},
"required": [
"current",
"previous",
],
"type": "object",
}
`);
});
test("overwrite id", () => {
const jobId = z.string().meta({ id: "aaa" });
const a = z.z.toJSONSchema(
z.object({
current: jobId,
previous: jobId.meta({ id: "bbb" }),
})
);
expect(a).toMatchInlineSnapshot(`
{
"$defs": {
"aaa": {
"id": "aaa",
"type": "string",
},
"bbb": {
"$ref": "#/$defs/aaa",
"id": "bbb",
},
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"current": {
"$ref": "#/$defs/aaa",
},
"previous": {
"$ref": "#/$defs/bbb",
},
},
"required": [
"current",
"previous",
],
"type": "object",
}
`);
const b = z.z.toJSONSchema(
z.object({
current: jobId,
previous: jobId.meta({ id: "ccc" }),
}),
{
reused: "ref",
}
);
expect(b).toMatchInlineSnapshot(`
{
"$defs": {
"aaa": {
"id": "aaa",
"type": "string",
},
"ccc": {
"$ref": "#/$defs/aaa",
"id": "ccc",
},
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"current": {
"$ref": "#/$defs/aaa",
},
"previous": {
"$ref": "#/$defs/ccc",
},
},
"required": [
"current",
"previous",
],
"type": "object",
}
`);
});
test("overwrite descriptions", () => {
const field = z.string().describe("a").describe("b").describe("c");
const a = z.z.toJSONSchema(
z.object({
d: field.describe("d"),
e: field.describe("e"),
})
);
expect(a).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"d": {
"description": "d",
"type": "string",
},
"e": {
"description": "e",
"type": "string",
},
},
"required": [
"d",
"e",
],
"type": "object",
}
`);
const b = z.z.toJSONSchema(
z.object({
d: field.describe("d"),
e: field.describe("e"),
}),
{
reused: "ref",
}
);
expect(b).toMatchInlineSnapshot(`
{
"$defs": {
"__schema0": {
"description": "c",
"type": "string",
},
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"d": {
"$ref": "#/$defs/__schema0",
"description": "d",
},
"e": {
"$ref": "#/$defs/__schema0",
"description": "e",
},
},
"required": [
"d",
"e",
],
"type": "object",
}
`);
});
test("top-level readonly", () => {
const A = z
.object({
name: z.string(),
get b() {
return B;
},
})
.readonly()
.meta({ id: "A" });
// z.globalRegistry.add(A, { id: "A" });
// .meta({ id: "A" });
const B = z
.object({
name: z.string(),
get a() {
return A;
},
})
.readonly()
.meta({ id: "B" });
// z.globalRegistry.add(B, { id: "B" });
// .meta({ id: "B" });
const result = z.z.toJSONSchema(A);
expect(result).toMatchInlineSnapshot(`
{
"$defs": {
"B": {
"additionalProperties": false,
"id": "B",
"properties": {
"a": {
"$ref": "#",
},
"name": {
"type": "string",
},
},
"readOnly": true,
"required": [
"name",
"a",
],
"type": "object",
},
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"id": "A",
"properties": {
"b": {
"$ref": "#/$defs/B",
},
"name": {
"type": "string",
},
},
"readOnly": true,
"required": [
"name",
"b",
],
"type": "object",
}
`);
});
test("basic registry", () => {
const myRegistry = z.registry<{ id: string }>();
const User = z.object({
name: z.string(),
get posts() {
return z.array(Post);
},
});
const Post = z.object({
title: z.string(),
content: z.string(),
get author() {
return User;
},
});
myRegistry.add(User, { id: "User" });
myRegistry.add(Post, { id: "Post" });
const result = z.z.toJSONSchema(myRegistry, { uri: (id) => `https://example.com/${id}.json` });
expect(result).toMatchInlineSnapshot(`
{
"schemas": {
"Post": {
"$id": "https://example.com/Post.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"author": {
"$ref": "https://example.com/User.json",
},
"content": {
"type": "string",
},
"title": {
"type": "string",
},
},
"required": [
"title",
"content",
"author",
],
"type": "object",
},
"User": {
"$id": "https://example.com/User.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
},
"posts": {
"items": {
"$ref": "https://example.com/Post.json",
},
"type": "array",
},
},
"required": [
"name",
"posts",
],
"type": "object",
},
},
}
`);
});
test("_ref", () => {
// const a = z.promise(z.string().describe("a"));
const a = z.z.toJSONSchema(z.promise(z.string().describe("a")));
expect(a).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "a",
"type": "string",
}
`);
const b = z.z.toJSONSchema(z.lazy(() => z.string().describe("a")));
expect(b).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "a",
"type": "string",
}
`);
const c = z.z.toJSONSchema(z.optional(z.string().describe("a")));
expect(c).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "a",
"type": "string",
}
`);
});
test("defaults/prefaults", () => {
const a = z
.string()
.transform((val) => val.length)
.pipe(z.number());
const b = a.prefault("hello");
const c = a.default(1234);
// a
expect(z.toJSONSchema(a)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "number",
}
`);
expect(z.toJSONSchema(a, { io: "input" })).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
}
`);
// b
expect(z.toJSONSchema(b)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "number",
}
`);
expect(z.toJSONSchema(b, { io: "input" })).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"default": "hello",
"type": "string",
}
`);
// c
expect(z.toJSONSchema(c)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"default": 1234,
"type": "number",
}
`);
expect(z.toJSONSchema(c, { io: "input" })).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
}
`);
});
test("input type", () => {
const schema = z.object({
a: z.string(),
b: z.string().optional(),
c: z.string().default("hello"),
d: z.string().nullable(),
e: z.string().prefault("hello"),
f: z.string().catch("hello"),
g: z.never(),
h: z.union([z.string(), z.number().default(2)]),
i: z.union([z.string(), z.string().optional()]),
});
expect(z.toJSONSchema(schema, { io: "input" })).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"a": {
"type": "string",
},
"b": {
"type": "string",
},
"c": {
"default": "hello",
"type": "string",
},
"d": {
"anyOf": [
{
"type": "string",
},
{
"type": "null",
},