zod
Version:
TypeScript-first schema declaration and validation library with static type inference
1,707 lines (1,611 loc) • 78.6 kB
text/typescript
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",
"pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.iso.time({ precision: -1 }))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.iso.time({ precision: 0 }))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d$",
"type": "string",
}
`);
expect(z.toJSONSchema(z.iso.time({ precision: 3 }))).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d\\.\\d{3}$",
"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",
"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("record with enum keys adds required", () => {
const schema = z.record(z.enum(["key1", "key2"]), z.number());
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": {
"type": "number",
},
"propertyNames": {
"enum": [
"key1",
"key2",
],
"type": "string",
},
"required": [
"key1",
"key2",
],
"type": "object",
}
`);
});
test("record filters enum values to strings and numbers for required", () => {
enum NumberEnum {
Zero = 0,
One = 1,
}
const schema = z.record(z.enum(NumberEnum), z.string());
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": {
"type": "string",
},
"propertyNames": {
"enum": [
0,
1,
],
"type": "number",
},
"required": [
0,
1,
],
"type": "object",
}
`);
});
test("strict record with regex key uses propertyNames", () => {
const schema = z.record(z.string().regex(/^label:[a-z]{2}$/), z.string());
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": {
"type": "string",
},
"propertyNames": {
"pattern": "^label:[a-z]{2}$",
"type": "string",
},
"type": "object",
}
`);
});
test("looseRecord with regex key uses patternProperties", () => {
const schema = z.looseRecord(z.string().regex(/^label:[a-z]{2}$/), z.string());
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"patternProperties": {
"^label:[a-z]{2}$": {
"type": "string",
},
},
"type": "object",
}
`);
});
test("looseRecord with multiple regex patterns uses patternProperties", () => {
const schema = z.looseRecord(
z
.string()
.regex(/^prefix_/)
.regex(/_suffix$/),
z.number()
);
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"patternProperties": {
"^prefix_": {
"type": "number",
},
"_suffix$": {
"type": "number",
},
},
"type": "object",
}
`);
});
test("looseRecord without regex key uses propertyNames", () => {
// looseRecord with plain string key should still use propertyNames
const schema = z.looseRecord(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("intersection of object with looseRecord uses patternProperties", () => {
const zLabeled = z.object({ label: z.string() });
const zLocalizedLabeled = z.looseRecord(z.string().regex(/^label:[a-z]{2}$/), z.string());
const schema = zLabeled.and(zLocalizedLabeled);
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"additionalProperties": false,
"properties": {
"label": {
"type": "string",
},
},
"required": [
"label",
],
"type": "object",
},
{
"patternProperties": {
"^label:[a-z]{2}$": {
"type": "string",
},
},
"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 inte