UNPKG

zod

Version:

TypeScript-first schema declaration and validation library with static type inference

1,917 lines (1,808 loc) • 59.3 kB
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", },