zod
Version:
TypeScript-first schema declaration and validation library with static type inference
1,468 lines • 50.5 kB
text/typescript
import { expect, test } from "vitest";
import { fromJSONSchema } from "../from-json-schema.js";
import * as z from "../index.js";
test("basic string schema", () => {
const schema = fromJSONSchema({ type: "string" });
expect(schema.parse("hello")).toBe("hello");
expect(() => schema.parse(123)).toThrow();
});
test("string with constraints", () => {
const schema = fromJSONSchema({
type: "string",
minLength: 3,
maxLength: 10,
pattern: "^[a-z]+$",
});
expect(schema.parse("hello")).toBe("hello");
expect(schema.parse("helloworld")).toBe("helloworld"); // exactly 10 chars - valid
expect(() => schema.parse("hi")).toThrow(); // too short
expect(() => schema.parse("helloworld1")).toThrow(); // too long (11 chars)
expect(() => schema.parse("Hello")).toThrow(); // pattern mismatch
});
test("pattern is not implicitly anchored", () => {
// JSON Schema patterns match anywhere in the string, not just the full string
const schema = fromJSONSchema({
type: "string",
pattern: "foo",
});
expect(schema.parse("foo")).toBe("foo");
expect(schema.parse("foobar")).toBe("foobar"); // matches at start
expect(schema.parse("barfoo")).toBe("barfoo"); // matches at end
expect(schema.parse("barfoobar")).toBe("barfoobar"); // matches in middle
expect(() => schema.parse("bar")).toThrow(); // no match
});
test("number schema", () => {
const schema = fromJSONSchema({ type: "number" });
expect(schema.parse(42)).toBe(42);
expect(() => schema.parse("42")).toThrow();
});
test("number with constraints", () => {
const schema = fromJSONSchema({
type: "number",
minimum: 0,
maximum: 100,
multipleOf: 5,
});
expect(schema.parse(50)).toBe(50);
expect(() => schema.parse(-1)).toThrow();
expect(() => schema.parse(101)).toThrow();
expect(() => schema.parse(47)).toThrow(); // not multiple of 5
});
test("draft-04 boolean exclusive bounds do not emit a redundant inclusive bound", () => {
const schema = fromJSONSchema({
type: "number",
minimum: 5,
maximum: 10,
exclusiveMinimum: true,
exclusiveMaximum: true,
});
// exclusive on both ends: boundaries rejected, interior accepted
expect(() => schema.parse(5)).toThrow();
expect(() => schema.parse(10)).toThrow();
expect(schema.parse(7)).toBe(7);
// the dominated inclusive bound would otherwise report a second, weaker issue alongside the exclusive one
expect(schema.safeParse(4).error!.issues).toHaveLength(1);
expect(schema.safeParse(11).error!.issues).toHaveLength(1);
// the inclusive .min()/.max() must be skipped so only the exclusive checks remain
const checks = (schema as any)._zod.def.checks.map((c: any) => [
c._zod.def.check,
c._zod.def.value,
c._zod.def.inclusive,
]);
expect(checks).toEqual([
["greater_than", 5, false],
["less_than", 10, false],
]);
});
test("integer schema", () => {
const schema = fromJSONSchema({ type: "integer" });
expect(schema.parse(42)).toBe(42);
expect(() => schema.parse(42.5)).toThrow();
});
test("boolean schema", () => {
const schema = fromJSONSchema({ type: "boolean" });
expect(schema.parse(true)).toBe(true);
expect(schema.parse(false)).toBe(false);
expect(() => schema.parse("true")).toThrow();
});
test("null schema", () => {
const schema = fromJSONSchema({ type: "null" });
expect(schema.parse(null)).toBe(null);
expect(() => schema.parse(undefined)).toThrow();
});
test("object schema", () => {
const schema = fromJSONSchema({
type: "object",
properties: {
name: { type: "string" },
age: { type: "number" },
},
required: ["name"],
});
expect(schema.parse({ name: "John", age: 30 })).toEqual({ name: "John", age: 30 });
expect(schema.parse({ name: "John" })).toEqual({ name: "John" });
expect(() => schema.parse({ age: 30 })).toThrow(); // missing required
});
test("object with additionalProperties false", () => {
const schema = fromJSONSchema({
type: "object",
properties: {
name: { type: "string" },
},
additionalProperties: false,
});
expect(schema.parse({ name: "John" })).toEqual({ name: "John" });
expect(() => schema.parse({ name: "John", extra: "field" })).toThrow();
});
test("array schema", () => {
const schema = fromJSONSchema({
type: "array",
items: { type: "string" },
});
expect(schema.parse(["a", "b", "c"])).toEqual(["a", "b", "c"]);
expect(() => schema.parse([1, 2, 3])).toThrow();
});
test("array with constraints", () => {
const schema = fromJSONSchema({
type: "array",
items: { type: "number" },
minItems: 2,
maxItems: 4,
});
expect(schema.parse([1, 2])).toEqual([1, 2]);
expect(schema.parse([1, 2, 3, 4])).toEqual([1, 2, 3, 4]);
expect(() => schema.parse([1])).toThrow();
expect(() => schema.parse([1, 2, 3, 4, 5])).toThrow();
});
test("tuple with prefixItems (draft-2020-12)", () => {
const schema = fromJSONSchema({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "array",
prefixItems: [{ type: "string" }, { type: "number" }],
});
expect(schema.parse([])).toEqual([]);
expect(schema.parse(["hello"])).toEqual(["hello"]);
expect(schema.parse(["hello", 42])).toEqual(["hello", 42]);
// no `items`, so the tail is unconstrained
expect(schema.parse(["hello", 42, "extra"])).toEqual(["hello", 42, "extra"]);
expect(() => schema.parse([1])).toThrow();
expect(() => schema.parse(["hello", "world"])).toThrow();
});
test("tuple with prefixItems and minItems (draft-2020-12)", () => {
const partialItems = fromJSONSchema({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "array",
prefixItems: [{ type: "string" }, { type: "number" }],
minItems: 1,
});
expect(() => partialItems.parse([])).toThrow();
expect(partialItems.parse(["hello"])).toEqual(["hello"]);
const allRequired = fromJSONSchema({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "array",
prefixItems: [{ type: "string" }, { type: "number" }],
minItems: 2,
});
expect(() => allRequired.parse(["hello"])).toThrow();
expect(allRequired.parse(["hello", 42])).toEqual(["hello", 42]);
});
test("tuple with prefixItems allows extra items by default (draft-2020-12)", () => {
const schema = fromJSONSchema({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "array",
prefixItems: [{ type: "string" }],
});
expect(schema.parse(["hello", 42, true])).toEqual(["hello", 42, true]);
});
test("tuple with prefixItems respects items schema (draft-2020-12)", () => {
const schema = fromJSONSchema({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "array",
prefixItems: [{ type: "string" }],
items: { type: "number" },
});
expect(schema.parse(["hello", 1, 2])).toEqual(["hello", 1, 2]);
expect(() => schema.parse(["hello", 1, "extra"])).toThrow();
});
test("tuple with prefixItems and items true allows extra items (draft-2020-12)", () => {
const schema = fromJSONSchema({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "array",
prefixItems: [{ type: "string" }, { type: "number" }],
items: true,
});
expect(schema.parse(["hello", 42, "extra", true])).toEqual(["hello", 42, "extra", true]);
});
test("tuple with prefixItems and items false rejects extra items (draft-2020-12)", () => {
const schema = fromJSONSchema({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "array",
prefixItems: [{ type: "string" }],
items: false,
});
expect(schema.parse(["hello"])).toEqual(["hello"]);
expect(() => schema.parse(["hello", 42])).toThrow();
});
test("tuple with items array (draft-7)", () => {
const schema = fromJSONSchema({
$schema: "http://json-schema.org/draft-07/schema#",
type: "array",
items: [{ type: "string" }, { type: "number" }],
additionalItems: false,
});
expect(schema.parse([])).toEqual([]);
expect(schema.parse(["hello"])).toEqual(["hello"]);
expect(schema.parse(["hello", 42])).toEqual(["hello", 42]);
expect(() => schema.parse([1])).toThrow();
expect(() => schema.parse(["hello", "world"])).toThrow();
expect(() => schema.parse(["hello", 42, "extra"])).toThrow();
});
test("tuple with items array and minItems (draft-7)", () => {
const partialItems = fromJSONSchema({
$schema: "http://json-schema.org/draft-07/schema#",
type: "array",
items: [{ type: "string" }, { type: "number" }],
additionalItems: false,
minItems: 1,
});
expect(() => partialItems.parse([])).toThrow();
expect(partialItems.parse(["hello"])).toEqual(["hello"]);
const allRequired = fromJSONSchema({
$schema: "http://json-schema.org/draft-07/schema#",
type: "array",
items: [{ type: "string" }, { type: "number" }],
additionalItems: false,
minItems: 2,
});
expect(() => allRequired.parse(["hello"])).toThrow();
expect(allRequired.parse(["hello", 42])).toEqual(["hello", 42]);
});
test("tuple with items array allows extra items by default (draft-7)", () => {
const schema = fromJSONSchema({
$schema: "http://json-schema.org/draft-07/schema#",
type: "array",
items: [{ type: "string" }],
});
expect(schema.parse(["hello", 42, true])).toEqual(["hello", 42, true]);
});
test("tuple with items array and additionalItems true allows extra items (draft-7)", () => {
const schema = fromJSONSchema({
$schema: "http://json-schema.org/draft-07/schema#",
type: "array",
items: [{ type: "string" }, { type: "number" }],
additionalItems: true,
});
expect(schema.parse(["hello", 42, "extra", true])).toEqual(["hello", 42, "extra", true]);
});
test("tuple with items array respects additionalItems schema (draft-7)", () => {
const schema = fromJSONSchema({
$schema: "http://json-schema.org/draft-07/schema#",
type: "array",
items: [{ type: "string" }],
additionalItems: { type: "number" },
});
expect(schema.parse(["hello", 1, 2])).toEqual(["hello", 1, 2]);
expect(() => schema.parse(["hello", 1, "extra"])).toThrow();
});
test("enum schema", () => {
const schema = fromJSONSchema({
enum: ["red", "green", "blue"],
});
expect(schema.parse("red")).toBe("red");
expect(() => schema.parse("yellow")).toThrow();
});
test("const schema", () => {
const schema = fromJSONSchema({
const: "hello",
});
expect(schema.parse("hello")).toBe("hello");
expect(() => schema.parse("world")).toThrow();
});
test("anyOf schema", () => {
const schema = fromJSONSchema({
anyOf: [{ type: "string" }, { type: "number" }],
});
expect(schema.parse("hello")).toBe("hello");
expect(schema.parse(42)).toBe(42);
expect(() => schema.parse(true)).toThrow();
});
test("allOf schema", () => {
const schema = fromJSONSchema({
allOf: [
{ type: "object", properties: { name: { type: "string" } }, required: ["name"] },
{ type: "object", properties: { age: { type: "number" } }, required: ["age"] },
],
});
const result = schema.parse({ name: "John", age: 30 }) as { name: string; age: number };
expect(result.name).toBe("John");
expect(result.age).toBe(30);
});
test("allOf with empty array", () => {
// Empty allOf without explicit type returns any
const schema1 = fromJSONSchema({
allOf: [],
});
expect(schema1.parse("hello")).toBe("hello");
expect(schema1.parse(123)).toBe(123);
expect(schema1.parse({})).toEqual({});
// Empty allOf with explicit type returns base schema
const schema2 = fromJSONSchema({
type: "string",
allOf: [],
});
expect(schema2.parse("hello")).toBe("hello");
expect(() => schema2.parse(123)).toThrow();
});
test("oneOf schema (exclusive union)", () => {
const schema = fromJSONSchema({
oneOf: [{ type: "string" }, { type: "number" }],
});
expect(schema.parse("hello")).toBe("hello");
expect(schema.parse(42)).toBe(42);
expect(() => schema.parse(true)).toThrow();
});
test("type with anyOf creates intersection", () => {
// type: string AND (type:string,minLength:5 OR type:string,pattern:^a)
const schema = fromJSONSchema({
type: "string",
anyOf: [
{ type: "string", minLength: 5 },
{ type: "string", pattern: "^a" },
],
});
// Should pass: string AND (minLength:5 OR pattern:^a) - matches minLength
expect(schema.parse("hello")).toBe("hello");
// Should pass: string AND (minLength:5 OR pattern:^a) - matches pattern
expect(schema.parse("abc")).toBe("abc");
// Should fail: string but neither minLength nor pattern match
expect(() => schema.parse("hi")).toThrow();
// Should fail: not a string
expect(() => schema.parse(123)).toThrow();
});
test("type with oneOf creates intersection", () => {
// type: string AND (exactly one of: type:string,minLength:5 OR type:string,pattern:^a)
const schema = fromJSONSchema({
type: "string",
oneOf: [
{ type: "string", minLength: 5 },
{ type: "string", pattern: "^a" },
],
});
// Should pass: string AND minLength:5 (exactly one match - "hello" length 5 >= 5, doesn't start with 'a')
expect(schema.parse("hello")).toBe("hello");
// Should pass: string AND pattern:^a (exactly one match - "abc" starts with 'a', length 3 < 5)
expect(schema.parse("abc")).toBe("abc");
// Should fail: string but neither match
expect(() => schema.parse("hi")).toThrow();
// Should fail: not a string
expect(() => schema.parse(123)).toThrow();
// Should fail: matches both (length >= 5 AND starts with 'a') - exclusive union fails
expect(() => schema.parse("apple")).toThrow();
});
test("unevaluatedItems throws error", () => {
expect(() => {
fromJSONSchema({
type: "array",
unevaluatedItems: false,
});
}).toThrow("unevaluatedItems is not supported");
});
test("unevaluatedProperties throws error", () => {
expect(() => {
fromJSONSchema({
type: "object",
unevaluatedProperties: false,
});
}).toThrow("unevaluatedProperties is not supported");
});
test("if/then/else throws error", () => {
expect(() => {
fromJSONSchema({
if: { type: "string" },
then: { type: "number" },
});
}).toThrow("Conditional schemas");
});
test("external $ref throws error", () => {
expect(() => {
fromJSONSchema({
$ref: "https://example.com/schema#/definitions/User",
});
}).toThrow("External $ref is not supported");
});
test("local $ref resolution", () => {
const schema = fromJSONSchema({
$defs: {
User: {
type: "object",
properties: {
name: { type: "string" },
},
required: ["name"],
},
},
$ref: "#/$defs/User",
});
expect(schema.parse({ name: "John" })).toEqual({ name: "John" });
expect(() => schema.parse({})).toThrow();
});
test("local $ref resolution unescapes JSON Pointer tokens", () => {
// `~1` must be decoded before `~0`, so the id "a~1b" survives as itself and not as "a/b".
for (const id of ["Shared/User", "My~Model", "a~b/c", "a~1b"]) {
const inner = z.object({ name: z.string() }).meta({ id });
const schema = fromJSONSchema(z.toJSONSchema(z.object({ inner })));
expect(schema.parse({ inner: { name: "John" } })).toEqual({ inner: { name: "John" } });
}
});
test("circular $ref with lazy", () => {
const schema = fromJSONSchema({
$defs: {
Node: {
type: "object",
properties: {
value: { type: "string" },
children: {
type: "array",
items: { $ref: "#/$defs/Node" },
},
},
},
},
$ref: "#/$defs/Node",
});
type Node = { value: string; children: Node[] };
const result = schema.parse({
value: "root",
children: [{ value: "child", children: [] }],
}) as Node;
expect(result.value).toBe("root");
expect(result.children[0]?.value).toBe("child");
});
test("patternProperties", () => {
const schema = fromJSONSchema({
type: "object",
patternProperties: {
"^S_": { type: "string" },
},
});
const result = schema.parse({ S_name: "John", S_age: "30" }) as Record<string, string>;
expect(result.S_name).toBe("John");
expect(result.S_age).toBe("30");
});
test("propertyNames with enum does not require keys", () => {
const schema = fromJSONSchema({
type: "object",
propertyNames: { enum: ["a", "b"] },
additionalProperties: { type: "string" },
});
expect(schema.parse({ a: "x" })).toEqual({ a: "x" });
expect(schema.parse({ b: "y" })).toEqual({ b: "y" });
expect(schema.parse({})).toEqual({});
expect(() => schema.parse({ c: "z" })).toThrow();
});
test("propertyNames with enum alongside properties", () => {
const schema = fromJSONSchema({
type: "object",
properties: { a: { type: "string" } },
propertyNames: { enum: ["a", "b"] },
additionalProperties: { type: "string" },
});
expect(schema.parse({ a: "x" })).toEqual({ a: "x" });
expect(schema.parse({})).toEqual({});
expect(() => schema.parse({ c: "z" })).toThrow();
expect(() => schema.parse({ a: "x", c: "z" })).toThrow();
});
test("propertyNames with enum still honors required", () => {
const schema = fromJSONSchema({
type: "object",
properties: { a: { type: "string" }, b: { type: "string" } },
required: ["a"],
propertyNames: { enum: ["a", "b"] },
additionalProperties: { type: "string" },
});
expect(schema.parse({ a: "x" })).toEqual({ a: "x" });
expect(schema.parse({ a: "x", b: "y" })).toEqual({ a: "x", b: "y" });
expect(() => schema.parse({})).toThrow();
expect(() => schema.parse({ b: "y" })).toThrow();
});
test("propertyNames does not apply additionalProperties to declared properties", () => {
const schema = fromJSONSchema({
type: "object",
properties: { a: { type: "number" } },
propertyNames: { enum: ["a", "b"] },
additionalProperties: { type: "string" },
});
expect(schema.parse({ a: 1 })).toEqual({ a: 1 });
expect(schema.parse({ a: 1, b: "x" })).toEqual({ a: 1, b: "x" });
expect(() => schema.parse({ b: 1 })).toThrow();
});
test("propertyNames alongside additionalProperties: false", () => {
const schema = fromJSONSchema({
type: "object",
properties: { a: { type: "string" } },
propertyNames: { enum: ["a", "b"] },
additionalProperties: false,
});
expect(schema.parse({ a: "x" })).toEqual({ a: "x" });
expect(() => schema.parse({ a: "x", b: "y" })).toThrow();
expect(() => schema.parse({ c: "z" })).toThrow();
});
test("propertyNames alongside patternProperties", () => {
const schema = fromJSONSchema({
type: "object",
patternProperties: { "^S_": { type: "string" } },
propertyNames: { type: "string", pattern: "^S_" },
});
expect(schema.parse({ S_a: "x" })).toEqual({ S_a: "x" });
expect(() => schema.parse({ S_a: 1 })).toThrow();
expect(() => schema.parse({ other: "x" })).toThrow();
});
test("propertyNames rejects keys the object parse would drop or synthesize", () => {
const schema = fromJSONSchema({ type: "object", propertyNames: { enum: ["a"] } });
// An object parse strips `__proto__`, so the guard has to see the raw input.
expect(() => schema.parse(JSON.parse('{"__proto__":1}'))).toThrow();
// A property `default` adds a key that was never in the instance.
const withDefault = fromJSONSchema({
type: "object",
properties: { AB: { type: "string", default: "x" } },
propertyNames: { type: "string", pattern: "^[a-z]+$" },
});
expect(withDefault.parse({})).toEqual({ AB: "x" });
});
test("propertyNames subschema without a type still constrains keys", () => {
const schema = fromJSONSchema({
type: "object",
propertyNames: { pattern: "^[A-Za-z_][A-Za-z0-9_]*$" },
});
expect(schema.parse({ valid_key: "x" })).toEqual({ valid_key: "x" });
expect(() => schema.parse({ "001 invalid": "x" })).toThrow();
});
test("boolean propertyNames", () => {
expect(() => fromJSONSchema({ type: "object", propertyNames: false }).parse({ a: 1 })).toThrow();
expect(fromJSONSchema({ type: "object", propertyNames: false }).parse({})).toEqual({});
expect(fromJSONSchema({ type: "object", propertyNames: true }).parse({ anything: 1 })).toEqual({ anything: 1 });
});
test("propertyNames metadata is not attached where it is inert", () => {
// Not an object: nothing enforces it, so it must not be advertised either.
expect(z.toJSONSchema(fromJSONSchema({ type: "string", propertyNames: { enum: ["a"] } }))).not.toHaveProperty(
"propertyNames"
);
// A $ref target is shared by every reference to it; metadata must not leak onto it.
const shared = z.toJSONSchema(
fromJSONSchema({
$defs: { N: { type: "object", properties: { v: { type: "string" } } } },
type: "object",
properties: { a: { $ref: "#/$defs/N", propertyNames: { enum: ["v"] } }, b: { $ref: "#/$defs/N" } },
})
) as any;
expect(shared.properties.b).not.toHaveProperty("propertyNames");
});
test("propertyNames survives a toJSONSchema round trip", () => {
const roundTripped = z.toJSONSchema(
fromJSONSchema({
type: "object",
propertyNames: { enum: ["a", "b"] },
additionalProperties: { type: "string" },
})
) as Record<string, unknown>;
expect(roundTripped.propertyNames).toEqual({ enum: ["a", "b"] });
expect(roundTripped.required).toBeUndefined();
// The key guard is a pipe; neither side may swallow the object it wraps.
for (const io of ["input", "output"] as const) {
const out = z.toJSONSchema(
fromJSONSchema({
type: "object",
properties: { a: { type: "string" } },
propertyNames: { enum: ["a", "b"] },
additionalProperties: { type: "string" },
}),
{ io }
) as Record<string, unknown>;
expect(out.type).toBe("object");
expect(out.properties).toEqual({ a: { type: "string" } });
expect(out.propertyNames).toEqual({ enum: ["a", "b"] });
}
});
test("patternProperties with regular properties", () => {
// Note: When patternProperties is combined with properties, the intersection validates all keys against the pattern. This test uses a pattern that matches the regular property name as well.
const schema = fromJSONSchema({
type: "object",
properties: {
S_name: { type: "string" },
},
patternProperties: {
"^S_": { type: "string" },
},
required: ["S_name"],
});
const result = schema.parse({ S_name: "John", S_extra: "value" }) as Record<string, string>;
expect(result.S_name).toBe("John");
expect(result.S_extra).toBe("value");
});
test("multiple patternProperties", () => {
const schema = fromJSONSchema({
type: "object",
patternProperties: {
"^S_": { type: "string" },
"^N_": { type: "number" },
},
});
const result = schema.parse({ S_name: "John", N_count: 123 }) as Record<string, string | number>;
expect(result.S_name).toBe("John");
expect(result.N_count).toBe(123);
// Keys not matching any pattern should pass through
const result2 = schema.parse({ S_name: "John", N_count: 123, other: "value" }) as Record<string, string | number>;
expect(result2.other).toBe("value");
});
test("multiple overlapping patternProperties", () => {
// If a key matches multiple patterns, value must satisfy all schemas
const schema = fromJSONSchema({
type: "object",
patternProperties: {
"^S_": { type: "string" },
"^S_N": { type: "string", minLength: 3 },
},
});
// S_name matches ^S_ but not ^S_N
expect(schema.parse({ S_name: "John" })).toEqual({ S_name: "John" });
// S_N matches both patterns - must satisfy both (string with minLength 3)
expect(schema.parse({ S_N: "abc" })).toEqual({ S_N: "abc" });
expect(() => schema.parse({ S_N: "ab" })).toThrow(); // too short for ^S_N pattern
});
test("patternProperties with additionalProperties false", () => {
const schema = fromJSONSchema({
type: "object",
patternProperties: {
"^S_": { type: "string" },
},
additionalProperties: false,
});
expect(schema.parse({ S_name: "ok" })).toEqual({ S_name: "ok" });
expect(() => schema.parse({ X_A: false })).toThrow();
expect(() => schema.parse({ S_name: 1 })).toThrow();
});
test("patternProperties and properties with additionalProperties false", () => {
const schema = fromJSONSchema({
type: "object",
properties: {
name: { type: "string" },
},
patternProperties: {
"^S_": { type: "string" },
},
additionalProperties: false,
required: ["name"],
});
expect(schema.parse({ name: "ok", S_count: "1" })).toEqual({ name: "ok", S_count: "1" });
expect(() => schema.parse({ name: "ok", S_count: 1 })).toThrow();
expect(() => schema.parse({ name: "ok", other: "value" })).toThrow();
});
test("multiple patternProperties with additionalProperties false", () => {
const schema = fromJSONSchema({
type: "object",
patternProperties: {
"^S_": { type: "string" },
"^N_": { type: "number" },
},
additionalProperties: false,
});
expect(schema.parse({ S_a: "a", N_b: 1 })).toEqual({ S_a: "a", N_b: 1 });
expect(() => schema.parse({ S_a: "a", N_b: 1, other: "value" })).toThrow();
});
test("default value", () => {
const schema = fromJSONSchema({
type: "string",
default: "hello",
});
// Default is applied during parsing if value is missing/undefined. This depends on Zod's default behavior
expect(schema.parse("world")).toBe("world");
});
test("description metadata", () => {
const schema = fromJSONSchema({
type: "string",
description: "A string value",
});
expect(schema.parse("hello")).toBe("hello");
});
test("version detection - draft-2020-12", () => {
const schema = fromJSONSchema({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "array",
prefixItems: [{ type: "string" }],
});
expect(schema.parse(["hello"])).toEqual(["hello"]);
});
test("version detection - draft-7", () => {
const schema = fromJSONSchema({
$schema: "http://json-schema.org/draft-07/schema#",
type: "array",
items: [{ type: "string" }],
});
expect(schema.parse(["hello"])).toEqual(["hello"]);
});
test("version detection - draft-4", () => {
const schema = fromJSONSchema({
$schema: "http://json-schema.org/draft-04/schema#",
type: "array",
items: [{ type: "string" }],
});
expect(schema.parse(["hello"])).toEqual(["hello"]);
});
test("default version (draft-2020-12)", () => {
const schema = fromJSONSchema({
type: "array",
prefixItems: [{ type: "string" }],
});
expect(schema.parse(["hello"])).toEqual(["hello"]);
});
test("string format - email", () => {
const schema = fromJSONSchema({
type: "string",
format: "email",
});
expect(schema.parse("test@example.com")).toBe("test@example.com");
});
test("string format - uuid", () => {
const schema = fromJSONSchema({
type: "string",
format: "uuid",
});
const uuid = "550e8400-e29b-41d4-a716-446655440000";
expect(schema.parse(uuid)).toBe(uuid);
});
test("string format - date-time", () => {
const schema = fromJSONSchema({
type: "string",
format: "date-time",
});
expect(schema.safeParse("2026-07-29T14:30:00Z").success).toBe(true);
expect(schema.safeParse("2026-07-29T16:30:00+02:00").success).toBe(true);
expect(schema.safeParse("2026-07-29T14:30:00").success).toBe(false);
expect(schema.safeParse("2026-07-29T16:30:00+0200").success).toBe(false);
const zuluOnly = fromJSONSchema({
type: "string",
format: "date-time",
pattern: "Z$",
});
expect(zuluOnly.safeParse("2026-07-29T14:30:00Z").success).toBe(true);
expect(zuluOnly.safeParse("2026-07-29T16:30:00+02:00").success).toBe(false);
expect(zuluOnly.safeParse("garbageZ").success).toBe(false);
const roundTripped = fromJSONSchema(z.toJSONSchema(z.iso.datetime({ offset: true })));
expect(roundTripped.safeParse("2026-07-29T16:30:00+02:00").success).toBe(true);
});
test("string format - time", () => {
const schema = fromJSONSchema({
type: "string",
format: "time",
});
// full-time, so seconds and an offset are both required
expect(schema.safeParse("14:30:00Z").success).toBe(true);
expect(schema.safeParse("16:30:00+02:00").success).toBe(true);
expect(schema.safeParse("16:30:00.123-05:30").success).toBe(true);
expect(schema.safeParse("14:30:00").success).toBe(false);
expect(schema.safeParse("14:30Z").success).toBe(false);
expect(schema.safeParse("16:30:00+0200").success).toBe(false);
expect(schema.safeParse("16:30:00+24:00").success).toBe(false);
});
test("string format - hostname", () => {
const schema = fromJSONSchema({
type: "string",
format: "hostname",
});
expect(schema.parse("example.com")).toBe("example.com");
expect(() => schema.parse("not a hostname!")).toThrow();
});
test("exclusiveMinimum and exclusiveMaximum", () => {
const schema = fromJSONSchema({
type: "number",
exclusiveMinimum: 0,
exclusiveMaximum: 100,
});
expect(schema.parse(50)).toBe(50);
expect(() => schema.parse(0)).toThrow();
expect(() => schema.parse(100)).toThrow();
});
test("boolean schema (true/false)", () => {
const trueSchema = fromJSONSchema(true);
expect(trueSchema.parse("anything")).toBe("anything");
const falseSchema = fromJSONSchema(false);
expect(() => falseSchema.parse("anything")).toThrow();
});
test("empty object schema", () => {
const schema = fromJSONSchema({
type: "object",
});
expect(schema.parse({})).toEqual({});
expect(schema.parse({ extra: "field" })).toEqual({ extra: "field" });
});
test("array without items", () => {
const schema = fromJSONSchema({
type: "array",
});
expect(schema.parse([1, "string", true])).toEqual([1, "string", true]);
});
test("mixed enum types", () => {
const schema = fromJSONSchema({
enum: ["string", 42, true, null],
});
expect(schema.parse("string")).toBe("string");
expect(schema.parse(42)).toBe(42);
expect(schema.parse(true)).toBe(true);
expect(schema.parse(null)).toBe(null);
});
test("nullable in OpenAPI 3.0", () => {
// General nullable case (not just enum: [null])
const stringSchema = fromJSONSchema(
{
type: "string",
nullable: true,
},
{ defaultTarget: "openapi-3.0" }
);
expect(stringSchema.parse("hello")).toBe("hello");
expect(stringSchema.parse(null)).toBe(null);
expect(() => stringSchema.parse(123)).toThrow();
const numberSchema = fromJSONSchema(
{
type: "number",
nullable: true,
},
{ defaultTarget: "openapi-3.0" }
);
expect(numberSchema.parse(42)).toBe(42);
expect(numberSchema.parse(null)).toBe(null);
expect(() => numberSchema.parse("string")).toThrow();
const objectSchema = fromJSONSchema(
{
type: "object",
properties: { name: { type: "string" } },
nullable: true,
},
{ defaultTarget: "openapi-3.0" }
);
expect(objectSchema.parse({ name: "John" })).toEqual({ name: "John" });
expect(objectSchema.parse(null)).toBe(null);
});
// Metadata extraction tests
test("unrecognized keys stored in globalRegistry by default", () => {
const schema = fromJSONSchema({
type: "string",
title: "My String",
deprecated: true,
examples: ["hello", "world"],
"x-custom": "custom value",
});
const meta = z.globalRegistry.get(schema);
expect(meta).toBeDefined();
expect(meta?.title).toBe("My String");
expect(meta?.deprecated).toBe(true);
expect(meta?.examples).toEqual(["hello", "world"]);
expect((meta as any)?.["x-custom"]).toBe("custom value");
// Clean up
z.globalRegistry.remove(schema);
});
test("unrecognized keys stored in custom registry", () => {
const customRegistry = z.registry<{ title?: string; deprecated?: boolean }>();
const schema = fromJSONSchema(
{
type: "number",
title: "Age",
deprecated: true,
},
{ registry: customRegistry }
);
// Should be in custom registry
const meta = customRegistry.get(schema);
expect(meta).toBeDefined();
expect(meta?.title).toBe("Age");
expect(meta?.deprecated).toBe(true);
// Should NOT be in globalRegistry
expect(z.globalRegistry.get(schema)).toBeUndefined();
});
test("$id and id are captured as metadata", () => {
const customRegistry = z.registry<{ $id?: string; id?: string }>();
const schema1 = fromJSONSchema(
{
$id: "https://example.com/schemas/user",
type: "object",
},
{ registry: customRegistry }
);
expect(customRegistry.get(schema1)?.$id).toBe("https://example.com/schemas/user");
const schema2 = fromJSONSchema(
{
id: "legacy-id",
type: "string",
},
{ registry: customRegistry }
);
expect(customRegistry.get(schema2)?.id).toBe("legacy-id");
});
test("x-* extension keys are captured as metadata", () => {
const customRegistry = z.registry<Record<string, unknown>>();
const schema = fromJSONSchema(
{
type: "string",
"x-openapi-example": "example value",
"x-internal": true,
"x-tags": ["api", "public"],
},
{ registry: customRegistry }
);
const meta = customRegistry.get(schema);
expect(meta?.["x-openapi-example"]).toBe("example value");
expect(meta?.["x-internal"]).toBe(true);
expect(meta?.["x-tags"]).toEqual(["api", "public"]);
});
test("metadata on nested schemas", () => {
const customRegistry = z.registry<Record<string, unknown>>();
const parentSchema = fromJSONSchema(
{
type: "object",
title: "User",
properties: {
name: {
type: "string",
title: "Name",
"x-field-order": 1,
},
age: {
type: "number",
title: "Age",
deprecated: true,
},
},
},
{ registry: customRegistry }
);
// Verify parent schema has its metadata
expect(customRegistry.get(parentSchema)?.title).toBe("User");
// We can't easily access nested schemas directly, but we can verify the registry is being used correctly by checking a separate schema
const simpleSchema = fromJSONSchema(
{
type: "string",
title: "Simple",
},
{ registry: customRegistry }
);
expect(customRegistry.get(simpleSchema)?.title).toBe("Simple");
});
test("no metadata added when no unrecognized keys", () => {
const customRegistry = z.registry<Record<string, unknown>>();
const schema = fromJSONSchema(
{
type: "string",
minLength: 1,
maxLength: 100,
description: "A regular string",
},
{ registry: customRegistry }
);
// description is handled via .describe(), so it shouldn't be in metadata. All other keys are recognized, so no metadata should be added
expect(customRegistry.get(schema)).toBeUndefined();
});
test("writeOnly and examples are captured as metadata", () => {
const customRegistry = z.registry<{ writeOnly?: boolean; examples?: unknown[] }>();
const schema = fromJSONSchema(
{
type: "string",
writeOnly: true,
examples: ["password123", "secret"],
},
{ registry: customRegistry }
);
const meta = customRegistry.get(schema);
expect(meta?.writeOnly).toBe(true);
expect(meta?.examples).toEqual(["password123", "secret"]);
});
test("$comment and $anchor are captured as metadata", () => {
const customRegistry = z.registry<{ $comment?: string; $anchor?: string }>();
const schema = fromJSONSchema(
{
type: "string",
$comment: "This is a developer note",
$anchor: "my-anchor",
},
{ registry: customRegistry }
);
const meta = customRegistry.get(schema);
expect(meta?.$comment).toBe("This is a developer note");
expect(meta?.$anchor).toBe("my-anchor");
});
test("contentEncoding and contentMediaType are stored as metadata", () => {
const customRegistry = z.registry<{ contentEncoding?: string; contentMediaType?: string }>();
const schema = fromJSONSchema(
{
type: "string",
contentEncoding: "base64",
contentMediaType: "image/png",
},
{ registry: customRegistry }
);
// Should just be a string schema
expect(schema.parse("aGVsbG8gd29ybGQ=")).toBe("aGVsbG8gd29ybGQ=");
// Content keywords should be in metadata
const meta = customRegistry.get(schema);
expect(meta?.contentEncoding).toBe("base64");
expect(meta?.contentMediaType).toBe("image/png");
});
test("description on enum schema is applied", () => {
const schema = fromJSONSchema({
enum: ["red", "green", "blue"],
description: "A color value",
});
expect(schema.description).toBe("A color value");
expect(schema.parse("red")).toBe("red");
});
test("description on const schema is applied", () => {
const schema = fromJSONSchema({
const: "hello",
description: "A greeting",
});
expect(schema.description).toBe("A greeting");
expect(schema.parse("hello")).toBe("hello");
});
test("description on not: {} (never) schema is applied", () => {
const schema = fromJSONSchema({
not: {},
description: "A never schema",
});
expect(schema.description).toBe("A never schema");
expect(() => schema.parse("anything")).toThrow();
});
test("default on enum schema is applied", () => {
const schema = fromJSONSchema({
enum: ["red", "green", "blue"],
default: "red",
});
expect(schema.parse(undefined)).toBe("red");
});
test("default on const schema is applied", () => {
const schema = fromJSONSchema({
const: "hello",
default: "hello",
});
expect(schema.parse(undefined)).toBe("hello");
});
test("description and default on enum schema are both applied", () => {
const schema = fromJSONSchema({
enum: ["red", "green", "blue"],
description: "A color value",
default: "red",
});
expect(schema.description).toBe("A color value");
expect(schema.parse(undefined)).toBe("red");
expect(schema.parse("green")).toBe("green");
});
test("description on type-array schema is applied", () => {
const schema = fromJSONSchema({
type: ["string", "number"],
description: "A string or number",
} as any);
expect(schema.description).toBe("A string or number");
expect(schema.parse("hello")).toBe("hello");
expect(schema.parse(42)).toBe(42);
});
test("default on type-array schema is applied", () => {
const schema = fromJSONSchema({
type: ["string", "number"],
default: "fallback",
} as any);
expect(schema.parse(undefined)).toBe("fallback");
expect(schema.parse("hello")).toBe("hello");
expect(schema.parse(42)).toBe(42);
});
test("description on schema with anyOf is applied to the outer schema", () => {
const schema = fromJSONSchema({
description: "Either a string or a number",
anyOf: [{ type: "string" }, { type: "number" }],
});
expect(schema.description).toBe("Either a string or a number");
expect(schema.parse("hello")).toBe("hello");
expect(schema.parse(42)).toBe(42);
});
test("default on schema with anyOf is applied to the outer schema", () => {
const schema = fromJSONSchema({
default: "fallback",
anyOf: [{ type: "string" }, { type: "number" }],
});
expect(schema.parse(undefined)).toBe("fallback");
expect(schema.parse(42)).toBe(42);
});
test("description and unrecognized metadata coexist on the same schema", () => {
const customRegistry = z.registry<{ "x-custom"?: string; description?: string }>();
const schema = fromJSONSchema(
{
type: "string",
description: "A custom string",
"x-custom": "value",
},
{ registry: customRegistry }
);
expect(schema.description).toBe("A custom string");
expect(customRegistry.get(schema)?.["x-custom"]).toBe("value");
});
test("circular input throws a clear error", () => {
const person: any = {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
};
person.properties.bestFriend = person;
expect(() => fromJSONSchema(person)).toThrow(/not valid JSON/);
});
test("getter-based input that synthesizes a cycle throws", () => {
const root: any = { type: "object", properties: { name: { type: "string" } } };
Object.defineProperty(root.properties, "self", {
enumerable: true,
get() {
return root;
},
});
expect(() => fromJSONSchema(root)).toThrow(/not valid JSON/);
});
test("BigInt in input throws", () => {
const input: any = { type: "integer", minimum: 1n };
expect(() => fromJSONSchema(input)).toThrow(/not valid JSON/);
});
test("class-instance input is normalized to a plain object", () => {
class StringSchema {
type = "string" as const;
minLength = 2;
}
const schema = fromJSONSchema(new StringSchema() as any);
expect(schema.parse("hi")).toBe("hi");
expect(() => schema.parse("h")).toThrow();
});
test("getter-based properties are materialized", () => {
const input: any = { type: "object", properties: {}, required: [] };
Object.defineProperty(input.properties, "name", {
enumerable: true,
get() {
return { type: "string" };
},
});
const schema = fromJSONSchema(input);
expect(schema.parse({ name: "Alice" })).toEqual({ name: "Alice" });
});
test("Date default is coerced to its JSON string form", () => {
const date = new Date("2026-01-02T03:04:05.000Z");
const schema = fromJSONSchema({ type: "string", default: date as any });
expect(schema.parse(undefined)).toBe(date.toISOString());
});
// An object literal can't express an own "__proto__" key — `{ __proto__: x }` sets the literal's prototype instead. JSON.parse is both the realistic source of a JSON Schema and the only way to write these cases.
test("required __proto__ property is represented in the shape but stripped during parsing", () => {
const schema = fromJSONSchema(
JSON.parse(`{
"type": "object",
"properties": { "__proto__": { "type": "string", "const": "admin" }, "role": { "type": "string" } },
"required": ["__proto__", "role"]
}`)
);
expect(Object.prototype.hasOwnProperty.call((schema as z.ZodObject).shape, "__proto__")).toBe(true);
expect(schema.parse({ role: "x" })).toEqual({ role: "x" });
expect(schema.parse(JSON.parse(`{ "role": "x", "__proto__": "wrong" }`))).toEqual({ role: "x" });
expect(schema.parse(JSON.parse(`{ "role": "x", "__proto__": "admin" }`))).toEqual({ role: "x" });
});
test("__proto__ annotation key reaches the registry", () => {
const registry = z.registry<Record<string, unknown>>();
const schema = fromJSONSchema(JSON.parse(`{ "type": "string", "__proto__": { "custom": 1 } }`), { registry });
const meta = registry.get(schema)!;
expect(Object.prototype.hasOwnProperty.call(meta, "__proto__")).toBe(true);
expect(meta.__proto__).toEqual({ custom: 1 });
});
test("minProperties and maxProperties count the raw input", () => {
const schema = fromJSONSchema({
type: "object",
properties: { a: { type: "string" } },
minProperties: 1,
maxProperties: 2,
});
expect(schema.safeParse({ a: "x" }).success).toBe(true);
expect(schema.safeParse({}).error!.issues[0]).toMatchObject({ code: "too_small", origin: "object", minimum: 1 });
expect(schema.safeParse({ a: "x", b: 1, c: 2 }).error!.issues[0]).toMatchObject({
code: "too_big",
origin: "object",
maximum: 2,
});
// raw input: a defaulted property does not satisfy the minimum, and a dropped __proto__ still counts
const defaulted = fromJSONSchema({
type: "object",
properties: { a: { type: "string", default: "x" } },
minProperties: 1,
});
expect(defaulted.safeParse({}).success).toBe(false);
const capped = fromJSONSchema({ type: "object", maxProperties: 1 });
expect(capped.safeParse(JSON.parse('{"__proto__":1,"a":1}')).success).toBe(false);
expect(z.toJSONSchema(schema)).toMatchObject({ minProperties: 1, maxProperties: 2 });
});
test("minProperties composes with propertyNames in one guard", () => {
const schema = fromJSONSchema({ type: "object", propertyNames: { pattern: "^a" }, minProperties: 1 });
expect(schema.safeParse({}).error!.issues[0]!.code).toBe("too_small");
expect(schema.safeParse({ b: 1 }).error!.issues[0]!.code).toBe("invalid_key");
expect(schema.safeParse({ a: 1 }).success).toBe(true);
});
test("uniqueItems rejects structural duplicates", () => {
const schema = fromJSONSchema({ type: "array", uniqueItems: true });
expect(
schema.safeParse([
{ a: 1, b: 2 },
{ b: 2, a: 1 },
]).error!.issues[0]
).toMatchObject({
code: "custom",
path: [1],
});
expect(schema.safeParse([1, "1", true, 1]).success).toBe(false);
expect(schema.safeParse([{ a: 1 }, { a: 2 }, [1], [2]]).success).toBe(true);
expect(z.toJSONSchema(schema)).toMatchObject({ uniqueItems: true });
});
test("contains with minContains and maxContains", () => {
const schema = fromJSONSchema({ type: "array", contains: { type: "integer" } });
expect(schema.safeParse(["a"]).success).toBe(false);
expect(schema.safeParse(["a", 1]).success).toBe(true);
const bounded = fromJSONSchema({ type: "array", contains: { type: "integer" }, minContains: 2, maxContains: 3 });
expect(bounded.safeParse([1, "a"]).success).toBe(false);
expect(bounded.safeParse([1, 2, "a"]).success).toBe(true);
expect(bounded.safeParse([1, 2, 3, 4]).success).toBe(false);
// minContains: 0 accepts an array with no matches at all
expect(fromJSONSchema({ type: "array", contains: { type: "integer" }, minContains: 0 }).safeParse([]).success).toBe(
true
);
expect(z.toJSONSchema(bounded)).toMatchObject({ contains: { type: "integer" }, minContains: 2, maxContains: 3 });
});
test("uniqueItems and contains see the raw instance, not the parsed output", () => {
const items = { type: "object", properties: { a: { type: "string", default: "x" } } } as const;
// no instance item carries `a`; the default must not synthesize the match
const contains = fromJSONSchema({
type: "array",
items,
contains: { type: "object", properties: { a: { const: "x" } }, required: ["a"] },
});
expect(contains.safeParse([{}]).success).toBe(false);
expect(contains.safeParse([{ a: "x" }]).success).toBe(true);
// the instance items differ; the default must not collapse them into duplicates
const unique = fromJSONSchema({ type: "array", items, uniqueItems: true });
expect(unique.safeParse([{ a: "x" }, {}]).success).toBe(true);
expect(unique.safeParse([{}, {}]).error!.issues[0]).toMatchObject({ code: "custom", path: [1] });
});
test("a guard keyword takes precedence over the length keywords", () => {
const schema = fromJSONSchema({ type: "array", items: { type: "integer" }, minItems: 3, uniqueItems: true });
// the guard runs on the input side of the pipe, so its issue aborts before minItems is reached
expect(schema.safeParse([1, 1]).error!.issues.map((i) => i.code)).toEqual(["custom"]);
expect(schema.safeParse([1, 2]).error!.issues.map((i) => i.code)).toEqual(["too_small"]);
});
test("a guard keyword drops default and examples from the input-mode document", () => {
// the guard is a transform on the pipe's input side, and toJSONSchema strips annotations from a transforming schema in input mode
const schema = fromJSONSchema({ type: "array", uniqueItems: true, default: [], examples: [[1]] });
expect(z.toJSONSchema(schema, { io: "input" })).not.toHaveProperty("default");
expect(z.toJSONSchema(schema, { io: "output" })).toMatchObject({ default: [], examples: [[1]] });
// without a guard keyword both modes carry them
const plain = fromJSONSchema({ type: "array", default: [], examples: [[1]] });
expect(z.toJSONSchema(plain, { io: "input" })).toMatchObject({ default: [], examples: [[1]] });
});
test("uniqueItems equality follows JSON semantics", () => {
const schema = fromJSONSchema({ type: "array", uniqueItems: true });
expect(schema.safeParse([1, "1", true, null]).success).toBe(true);
expect(schema.safeParse([{ a: undefined }, {}]).success).toBe(true);
expect(
schema.safeParse([
[1, [2]],
[1, [2]],
]).success
).toBe(false);
expect(schema.safeParse([{ a: "b=c" }, { "a=b": "c" }]).success).toBe(true);
// every duplicate is reported against the first element it matches
expect(schema.safeParse([1, 1, 1]).error!.issues.map((i) => i.path[0])).toEqual([1, 2]);
// a cycle cannot be compared, so it is never called a duplicate rather than hanging
const cyclic: any = {};
cyclic.self = cyclic;
expect(schema.safeParse([cyclic, cyclic]).success).toBe(true);
});
test("a carried subschema that references $defs is dropped rather than emitted dangling", () => {
const schema = fromJSONSchema({
type: "array",
contains: { $ref: "#/$defs/Hit" },
$defs: { Hit: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
});
// enforcement is unaffected; only the round trip gives the keyword up
expect(schema.safeParse([{ id: "a" }]).success).toBe(true);
expect(schema.safeParse([{}]).success).toBe(false);
expect(JSON.stringify(z.toJSONSchema(schema))).not.toContain("$ref");
expect(z.toJSONSchema(schema)).not.toHaveProperty("contains");
// an inline subschema is self-contained, so it still round-trips
const inline = fromJSONSchema({ type: "array", contains: { type: "integer" } });
expect(z.toJSONSchema(inline)).toMatchObject({ contains: { type: "integer" } });
// only schema positions are walked, so a `$ref` key in instance data or an unknown annotation still travels
for (const annotation of [{ default: { $ref: "literal" } }, { "x-note": { $ref: "literal" } }]) {
const annotated = fromJSONSchema({ type: "array", contains: { type: "integer", ...annotation } });
expect(annotated.safeParse(["x"]).success).toBe(false);
expect(z.toJSONSchema(annotated)).toMatchObject({ contains: { type: "integer" } });
}
// draft-7 `dependencies` is a schema map; its array form names properties and holds no reference
const dependent = fromJSONSchema({
type: "array",
contains: { type: "object", dependencies: { a: { $ref: "#/definitions/Hit" } } },
definitions: { Hit: { type: "object" } },
});
expect(z.toJSONSchema(dependent)).not.toHaveProperty("contains");
const named = fromJSONSchema({
type: "array",
contains: { type: "object", dependencies: { a: ["b"] } },
});
expect(z.toJSONSchema(named)).toMatchObject({ contains: { dependencies: { a: ["b"] } } });
// a reference nested in a schema position is still caught
const nested = fromJSONSchema({
type: "array",
contains: { type: "array", items: { $ref: "#/$defs/Hit" } },
$defs: { Hit: { type: "string" } },
});
expect(z.toJSONSchema(nested)).not.toHaveProperty("contains");
});