Skip to main content
Glama
from-json-schema.test.ts16.1 kB
import { expect, test } from "vitest"; import { fromJSONSchema } from "../from-json-schema.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("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(["hello", 42])).toEqual(["hello", 42]); expect(() => schema.parse(["hello"])).toThrow(); expect(() => schema.parse(["hello", "world"])).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(["hello", 42])).toEqual(["hello", 42]); expect(() => schema.parse(["hello", 42, "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("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("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("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("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); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ali-48/rag-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server