UNPKG

zod

Version:

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

1,758 lines (1,654 loc) • 72.4 kB
import { Validator } from "@seriousme/openapi-schema-validator"; import { describe, expect, test } from "vitest"; import * as z from "zod"; // import * as zCore from "zod/v4/core"; const openAPI30Validator = new Validator(); /** @see https://github.com/colinhacks/zod/issues/5147 */ const validateOpenAPI30Schema = async (zodJSONSchema: Record<string, unknown>): Promise<true> => { const res = await openAPI30Validator.validate({ openapi: "3.0.0", info: { title: "SampleApi", description: "Sample backend service", version: "1.0.0", }, components: { schemas: { test: zodJSONSchema } }, paths: {}, }); if (!res.valid) { // `console.error` should make `vitest` trow an unhandled error // printing the validation messages in consoles console.error( `OpenAPI schema is not valid against ${openAPI30Validator.version}`, JSON.stringify(res.errors, null, 2) ); } return true; }; 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}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$", "type": "string", } `); expect(z.toJSONSchema(z.mac())).toMatchInlineSnapshot(` { "$schema": "https://json-schema.org/draft/2020-12/schema", "format": "mac", "pattern": "^(?:[0-9A-F]{2}:){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$", "type": "string", } `); expect(z.toJSONSchema(z.mac({ delimiter: ":" }))).toMatchInlineSnapshot(` { "$schema": "https://json-schema.org/draft/2020-12/schema", "format": "mac", "pattern": "^(?:[0-9A-F]{2}:){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$", "type": "string", } `); expect(z.toJSONSchema(z.mac({ delimiter: "-" }))).toMatchInlineSnapshot(` { "$schema": "https://json-schema.org/draft/2020-12/schema", "format": "mac", "pattern": "^(?:[0-9A-F]{2}-){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}-){5}[0-9a-f]{2}$", "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|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "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|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "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}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$", "type": "string", } `); expect(z.toJSONSchema(z.mac())).toMatchInlineSnapshot(` { "$schema": "https://json-schema.org/draft/2020-12/schema", "format": "mac", "pattern": "^(?:[0-9A-F]{2}:){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$", "type": "string", } `); expect(z.toJSONSchema(z.mac({ delimiter: ":" }))).toMatchInlineSnapshot(` { "$schema": "https://json-schema.org/draft/2020-12/schema", "format": "mac", "pattern": "^(?:[0-9A-F]{2}:){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$", "type": "string", } `); expect(z.toJSONSchema(z.mac({ delimiter: "-" }))).toMatchInlineSnapshot(` { "$schema": "https://json-schema.org/draft/2020-12/schema", "format": "mac", "pattern": "^(?:[0-9A-F]{2}-){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}-){5}[0-9a-f]{2}$", "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("number constraints draft-4", () => { expect(z.toJSONSchema(z.number().gt(5).lt(10), { target: "draft-4" })).toMatchInlineSnapshot(` { "$schema": "http://json-schema.org/draft-04/schema#", "exclusiveMaximum": true, "exclusiveMinimum": true, "maximum": 10, "minimum": 5, "type": "number", } `); }); test("target normalization draft-04 and draft-07", () => { // Test that both old (draft-4, draft-7) and new (draft-04, draft-07) target formats work // Test draft-04 / draft-4 expect(z.toJSONSchema(z.number().gt(5), { target: "draft-04" })).toMatchInlineSnapshot(` { "$schema": "http://json-schema.org/draft-04/schema#", "exclusiveMinimum": true, "minimum": 5, "type": "number", } `); expect(z.toJSONSchema(z.number().gt(5), { target: "draft-4" })).toMatchInlineSnapshot(` { "$schema": "http://json-schema.org/draft-04/schema#", "exclusiveMinimum": true, "minimum": 5, "type": "number", } `); // Test draft-07 / draft-7 expect(z.toJSONSchema(z.number().gt(5), { target: "draft-07" })).toMatchInlineSnapshot(` { "$schema": "http://json-schema.org/draft-07/schema#", "exclusiveMinimum": 5, "type": "number", } `); expect(z.toJSONSchema(z.number().gt(5), { target: "draft-7" })).toMatchInlineSnapshot(` { "$schema": "http://json-schema.org/draft-07/schema#", "exclusiveMinimum": 5, "type": "number", } `); }); test("nullable openapi-3.0", () => { const schema = z.string().nullable(); const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" }); validateOpenAPI30Schema(jsonSchema); expect(jsonSchema).toMatchInlineSnapshot(` { "nullable": true, "type": "string", } `); }); test("union with null openapi-3.0", () => { const schema = z.union([z.string(), z.null()]); const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" }); validateOpenAPI30Schema(jsonSchema); expect(jsonSchema).toMatchInlineSnapshot(` { "anyOf": [ { "type": "string", }, { "enum": [ null, ], "nullable": true, "type": "string", }, ], } `); }); test("number with exclusive min-max openapi-3.0", () => { const schema = z.number().lt(100).gt(1); const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" }); validateOpenAPI30Schema(jsonSchema); expect(jsonSchema).toMatchInlineSnapshot(` { "exclusiveMaximum": true, "exclusiveMinimum": true, "maximum": 100, "minimum": 1, "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("discriminated unions", () => { const schema = z.discriminatedUnion("type", [ z.object({ type: z.literal("success"), data: z.string() }), z.object({ type: z.literal("error"), message: z.string() }), ]); expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(` { "$schema": "https://json-schema.org/draft/2020-12/schema", "oneOf": [ { "additionalProperties": false, "properties": { "data": { "type": "string", }, "type": { "const": "success", "type": "string", }, }, "required": [ "type", "data", ], "type": "object", }, { "additionalProperties": false, "properties": { "message": { "type": "string", }, "type": { "const": "error", "type": "string", }, }, "required": [ "type", "message", ], "type": "object", }, ], } `); }); 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("record openapi-3.0", () => { const schema = z.record(z.string(), z.boolean()); const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" }); validateOpenAPI30Schema(jsonSchema); expect(jsonSchema).toMatchInlineSnapshot(` { "additionalProperties": { "type": "boolean", }, "type": "object", } `); }); test("tuple", () => { const schema = z.tuple([z.string(), z.number()]); expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(` { "$schema": "https://json-schema.org/draft/2020-12/schema", "prefixItems": [ { "type": "string", }, { "type": "number", }, ], "type": "array", } `); }); test("tuple with rest", () => { 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("tuple openapi-3.0", () => { const schema = z.tuple([z.string(), z.number()]); const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" }); validateOpenAPI30Schema(jsonSchema); expect(jsonSchema).toMatchInlineSnapshot(` { "items": { "anyOf": [ { "type": "string", }, { "type": "number", }, ], }, "maxItems": 2, "minItems": 2, "type": "array", } `); }); test("tuple with rest openapi-3.0", () => { const schema = z.tuple([z.string(), z.number()]).rest(z.boolean()); const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" }); validateOpenAPI30Schema(jsonSchema); expect(jsonSchema).toMatchInlineSnapshot(` { "items": { "anyOf": [ { "type": "string", }, { "type": "number", }, { "type": "boolean", }, ], }, "minItems": 3, "type": "array", } `); }); test("tuple with null openapi-3.0", () => { const schema = z.tuple([z.string(), z.number(), z.null()]); const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" }); validateOpenAPI30Schema(jsonSchema); expect(jsonSchema).toMatchInlineSnapshot(` { "items": { "anyOf": [ { "type": "string", }, { "type": "number", }, { "enum": [ null, ], "nullable": true, "type": "string", }, ], }, "maxItems": 3, "minItems": 3, "type": "array", } `); }); test("tuple draft-7", () => { const schema = z.tuple([z.string(), z.number()]); expect(z.toJSONSchema(schema, { target: "draft-7", io: "input" })).toMatchInlineSnapshot(` { "$schema": "http://json-schema.org/draft-07/schema#", "items": [ { "type": "string", }, { "type": "number", }, ], "type": "array", } `); }); test("tuple with rest draft-7", () => { const schema = z.tuple([z.string(), z.number()]).rest(z.boolean()); expect(z.toJSONSchema(schema, { target: "draft-7", io: "input" })).toMatchInlineSnapshot(` { "$schema": "http://json-schema.org/draft-07/schema#", "additionalItems": { "type": "boolean", }, "items": [ { "type": "string", }, { "type": "number", }, ], "type": "array", } `); }); test("tuple with rest draft-7 - issue #5151 regression test", () => { // This test addresses issue #5151: tuple with rest elements and ids // in draft-7 had incorrect internal path handling affecting complex scenarios const primarySchema = z.string().meta({ id: "primary" }); const restSchema = z.number().meta({ id: "rest" }); const testSchema = z.tuple([primarySchema], restSchema); // Test both final output structure AND internal path handling const capturedPaths: string[] = []; const result = z.toJSONSchema(testSchema, { target: "draft-7", override: (ctx) => capturedPaths.push(ctx.path.join("/")), }); // Verify correct draft-7 structure with metadata extraction expect(result).toMatchInlineSnapshot(` { "$schema": "http://json-schema.org/draft-07/schema#", "additionalItems": { "$ref": "#/definitions/rest", }, "definitions": { "primary": { "id": "primary", "type": "string", }, "rest": { "id": "rest", "type": "number", }, }, "items": [ { "$ref": "#/definitions/primary", }, ], "type": "array", } `); // Verify internal paths are correct (this was the actual bug) expect(capturedPaths).toContain("items/0"); // prefix items should use "items" path expect(capturedPaths).toContain("additionalItems"); // rest should use "additionalItems" path expect(capturedPaths).not.toContain("prefixItems/0"); // should not use draft-2020-12 paths // Structural validations expect(Array.isArray(result.items)).toBe(true); expect(result.additionalItems).toBeDefined(); }); 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", } `); }); test("literal draft-4", () => { const a = z.literal("hello"); expect(z.toJSONSchema(a, { target: "draft-4" })).toMatchInlineSnapshot(` { "$schema": "http://json-schema.org/draft-04/schema#", "enum": [ "hello", ], "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.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.toJSONSchema(schema, { unrepresentable: "any", io: "input", override(_) { overrideCount++; }, }); expect(overrideCount).toBe(6); }); test("override with refs", () => { const a = z.string().optional(); const result = 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.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.toJSONSchema(mySchema); expect(a).toMatchInlineSnapshot(` { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "number", } `); // => { type: "number" } const b = 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 Exter