Buckets:
ktongue/docker_container / .opencode /node_modules /zod /src /v4 /classic /tests /to-json-schema.test.ts
| import { Validator } from "@seriousme/openapi-schema-validator"; | |
| import { describe, expect, test } from "vitest"; | |
| import * as z from "zod/v4"; | |
| // 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.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.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("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("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.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", | |
| }, | |
| ], | |
| }, | |
| "e": { | |
| "default": "hello", | |
| "type": "string", | |
| }, | |
| "f": { | |
| "default": "hello", | |
| "type": "string", | |
| }, | |
| "g": { | |
| "not": {}, | |
| }, | |
| "h": { | |
| "anyOf": [ | |
| { | |
| "type": "string", | |
| }, | |
| { | |
| "default": 2, | |
| "type": "number", | |
| }, | |
| ], | |
| }, | |
| "i": { | |
| "anyOf": [ | |
| { | |
| "type": "string", | |
| }, | |
| { | |
| "type": "string", | |
| }, | |
| ], | |
| }, | |
| }, | |
| "required": [ | |
| "a", | |
| "d", | |
| "f", | |
| "g", | |
| ], | |
| "type": "object", | |
| } | |
| `); | |
| expect(z.toJSONSchema(schema, { io: "output" })).toMatchInlineSnapshot(` | |
| { | |
| "$schema": "https://json-schema.org/draft/2020-12/schema", | |
| "additionalProperties": false, | |
| "properties": { | |
| "a": { | |
| "type": "string", | |
| }, | |
| "b": { | |
| "type": "string", | |
| }, | |
| "c": { | |
| "default": "hello", | |
| "type": "string", | |
| }, | |
| "d": { | |
| "anyOf": [ | |
| { | |
| "type": "string", | |
| }, | |
| { | |
| "type": "null", | |
| }, | |
| ], | |
| }, | |
| "e": { | |
| "type": "string", | |
| }, | |
| "f": { | |
| "default": "hello", | |
| "type": "string", | |
| }, | |
| "g": { | |
| "not": {}, | |
| }, | |
| "h": { | |
| "anyOf": [ | |
| { | |
| "type": "string", | |
| }, | |
| { | |
| "default": 2, | |
| "type": "number", | |
| }, | |
| ], | |
| }, | |
| "i": { | |
| "anyOf": [ | |
| { | |
| "type": "string", | |
| }, | |
| { | |
| "type": "string", | |
| }, | |
| ], | |
| }, | |
| }, | |
| "required": [ | |
| "a", | |
| "c", | |
| "d", | |
| "e", | |
| "f", | |
| "g", | |
| "h", | |
| ], | |
| "type": "object", | |
| } | |
| `); | |
| }); | |
| test("examples on pipe", () => { | |
| const schema = z | |
| .string() | |
| .meta({ examples: ["test"] }) | |
| .transform(Number) | |
| // .pipe(z.transform(Number).meta({ examples: [4] })) | |
| .meta({ examples: [4] }); | |
| const i = z.z.toJSONSchema(schema, { io: "input", unrepresentable: "any" }); | |
| expect(i).toMatchInlineSnapshot(` | |
| { | |
| "$schema": "https://json-schema.org/draft/2020-12/schema", | |
| "examples": [ | |
| "test", | |
| ], | |
| "type": "string", | |
| } | |
| `); | |
| const o = z.z.toJSONSchema(schema, { io: "output", unrepresentable: "any" }); | |
| expect(o).toMatchInlineSnapshot(` | |
| { | |
| "$schema": "https://json-schema.org/draft/2020-12/schema", | |
| "examples": [ | |
| 4, | |
| ], | |
| } | |
| `); | |
| }); | |
| // test("number checks", () => { | |
| // expect(z.z.toJSONSchema(z.number().int())).toMatchInlineSnapshot(` | |
| // { | |
| // "maximum": 9007199254740991, | |
| // "minimum": -9007199254740991, | |
| // "type": "integer", | |
| // } | |
| // `); | |
| // expect(z.z.toJSONSchema(z.int())).toMatchInlineSnapshot(` | |
| // { | |
| // "maximum": 9007199254740991, | |
| // "minimum": -9007199254740991, | |
| // "type": "integer", | |
| // } | |
| // `); | |
| // expect(z.z.toJSONSchema(z.int().positive())).toMatchInlineSnapshot(` | |
| // { | |
| // "exclusiveMinimum": 0, | |
| // "maximum": 9007199254740991, | |
| // "minimum": -9007199254740991, | |
| // "type": "integer", | |
| // } | |
| // `); | |
| // expect(z.z.toJSONSchema(z.int().nonnegative())).toMatchInlineSnapshot(` | |
| // { | |
| // "maximum": 9007199254740991, | |
| // "minimum": 0, | |
| // "type": "integer", | |
| // } | |
| // `); | |
| // expect(z.z.toJSONSchema(z.int().gt(0))).toMatchInlineSnapshot(` | |
| // { | |
| // "exclusiveMinimum": 0, | |
| // "maximum": 9007199254740991, | |
| // "minimum": -9007199254740991, | |
| // "type": "integer", | |
| // } | |
| // `); | |
| // expect(z.z.toJSONSchema(z.int().gte(0))).toMatchInlineSnapshot(` | |
| // { | |
| // "maximum": 9007199254740991, | |
| // "minimum": 0, | |
| // "type": "integer", | |
| // } | |
| // `); | |
| // }); | |
| test("use output type for preprocess", () => { | |
| const a = z.preprocess((val) => String(val), z.string()); | |
| expect(z.toJSONSchema(a, { io: "input" })).toMatchInlineSnapshot(` | |
| { | |
| "$schema": "https://json-schema.org/draft/2020-12/schema", | |
| "type": "string", | |
| } | |
| `); | |
| }); | |
| // test("isTransforming", () => { | |
| // const tx = z.core.isTransforming; | |
| // expect(tx(z.string())).toEqual(false); | |
| // expect(tx(z.string().transform((val) => val))).toEqual(true); | |
| // expect(tx(z.string().pipe(z.string()))).toEqual(false); | |
| // expect( | |
| // tx( | |
| // z | |
| // .string() | |
| // .transform((val) => val) | |
| // .pipe(z.string()) | |
| // ) | |
| // ).toEqual(true); | |
| // const a = z.transform((val) => val); | |
| // expect(tx(z.transform((val) => val))).toEqual(true); | |
| // expect(tx(a.optional())).toEqual(true); | |
| // const b = z.string().optional(); | |
| // expect(tx(b)).toEqual(false); | |
| // const c = z.string().prefault("hello"); | |
| // expect(tx(c)).toEqual(false); | |
| // const d = z.string().default("hello"); | |
| // expect(tx(d)).toEqual(false); | |
| // }); | |
| test("flatten simple intersections", () => { | |
| const FirstSchema = z.object({ | |
| testNum: z.number(), | |
| }); | |
| const SecondSchema = z.object({ | |
| testStr: z.string(), | |
| }); | |
| const ThirdSchema = z.object({ | |
| testBool: z.boolean(), | |
| }); | |
| const HelloSchema = FirstSchema.and(SecondSchema).and(ThirdSchema).describe("123"); | |
| // Zod 3 | |
| // console.log(JSON.stringify(zodToJsonSchema(HelloSchema), null, 2)); | |
| // Zod 4 | |
| const result = z.toJSONSchema(HelloSchema, { target: "draft-7" }); | |
| expect(result).toMatchInlineSnapshot(` | |
| { | |
| "$schema": "http://json-schema.org/draft-07/schema#", | |
| "allOf": [ | |
| { | |
| "additionalProperties": false, | |
| "properties": { | |
| "testNum": { | |
| "type": "number", | |
| }, | |
| }, | |
| "required": [ | |
| "testNum", | |
| ], | |
| "type": "object", | |
| }, | |
| { | |
| "additionalProperties": false, | |
| "properties": { | |
| "testStr": { | |
| "type": "string", | |
| }, | |
| }, | |
| "required": [ | |
| "testStr", | |
| ], | |
| "type": "object", | |
| }, | |
| { | |
| "additionalProperties": false, | |
| "properties": { | |
| "testBool": { | |
| "type": "boolean", | |
| }, | |
| }, | |
| "required": [ | |
| "testBool", | |
| ], | |
| "type": "object", | |
| }, | |
| ], | |
| "description": "123", | |
| } | |
| `); | |
| }); | |
| test("z.file()", () => { | |
| const a = z.file(); | |
| expect(z.toJSONSchema(a)).toMatchInlineSnapshot(` | |
| { | |
| "$schema": "https://json-schema.org/draft/2020-12/schema", | |
| "contentEncoding": "binary", | |
| "format": "binary", | |
| "type": "string", | |
| } | |
| `); | |
| const b = z.file().mime("image/png").min(1000).max(10000); | |
| expect(z.toJSONSchema(b)).toMatchInlineSnapshot(` | |
| { | |
| "$schema": "https://json-schema.org/draft/2020-12/schema", | |
| "contentEncoding": "binary", | |
| "contentMediaType": "image/png", | |
| "format": "binary", | |
| "maxLength": 10000, | |
| "minLength": 1000, | |
| "type": "string", | |
| } | |
| `); | |
| const c = z.file().mime(["image/png", "image/jpg"]).min(1000).max(10000); | |
| expect(z.toJSONSchema(c)).toMatchInlineSnapshot(` | |
| { | |
| "$schema": "https://json-schema.org/draft/2020-12/schema", | |
| "anyOf": [ | |
| { | |
| "contentEncoding": "binary", | |
| "contentMediaType": "image/png", | |
| "format": "binary", | |
| "maxLength": 10000, | |
| "minLength": 1000, | |
| "type": "string", | |
| }, | |
| { | |
| "contentEncoding": "binary", | |
| "contentMediaType": "image/jpg", | |
| "format": "binary", | |
| "maxLength": 10000, | |
| "minLength": 1000, | |
| "type": "string", | |
| }, | |
| ], | |
| } | |
| `); | |
| }); | |
| test("custom toJSONSchema", () => { | |
| const schema = z.instanceof(Date); | |
| schema._zod.toJSONSchema = () => ({ | |
| type: "string", | |
| format: "date-time", | |
| }); | |
| expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(` | |
| { | |
| "$schema": "https://json-schema.org/draft/2020-12/schema", | |
| "format": "date-time", | |
| "type": "string", | |
| } | |
| `); | |
| }); | |
| test("cycle detection - root", () => { | |
| const schema = z.object({ | |
| name: z.string(), | |
| get subcategories() { | |
| return z.array(schema); | |
| }, | |
| }); | |
| expect(() => z.toJSONSchema(schema, { cycles: "throw" })).toThrowErrorMatchingInlineSnapshot(` | |
| [Error: Cycle detected: #/properties/subcategories/items/<root> | |
| Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.] | |
| `); | |
| }); | |
| test("cycle detection - mutual recursion", () => { | |
| const A = z.object({ | |
| name: z.string(), | |
| get subcategories() { | |
| return z.array(B); | |
| }, | |
| }); | |
| const B = z.object({ | |
| name: z.string(), | |
| get subcategories() { | |
| return z.array(A); | |
| }, | |
| }); | |
| expect(() => z.toJSONSchema(A, { cycles: "throw" })).toThrowErrorMatchingInlineSnapshot(` | |
| [Error: Cycle detected: #/properties/subcategories/items/properties/subcategories/items/<root> | |
| Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.] | |
| `); | |
| }); | |
Xet Storage Details
- Size:
- 68.2 kB
- Xet hash:
- 76ecf2479943ef86623aaaccb07723b5efdd9b222112979b96d98aab9323090d
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.