Skip to content

Commit 3a7ee56

Browse files
committed
Support more JSON Schema features
1 parent 5092c78 commit 3a7ee56

File tree

2 files changed

+184
-7
lines changed

2 files changed

+184
-7
lines changed

src/index.test.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,113 @@ describe("convertJsonSchemaToZod", () => {
7676
// For enums, we just check that the original enum values are present
7777
expect(resultSchema.enum).toEqual(jsonSchema.enum);
7878
});
79-
});
79+
80+
// Tests for unimplemented but supported features
81+
describe("String validation", () => {
82+
it("should support minLength and maxLength constraints", () => {
83+
const jsonSchema = {
84+
$schema: "http://json-schema.org/draft-07/schema#",
85+
type: "string",
86+
minLength: 3,
87+
maxLength: 10
88+
};
89+
90+
const zodSchema = convertJsonSchemaToZod(jsonSchema);
91+
const resultSchema = zodToJsonSchema(zodSchema);
92+
expect(resultSchema).toEqual(jsonSchema);
93+
});
94+
95+
it("should support pattern constraint", () => {
96+
const jsonSchema = {
97+
$schema: "http://json-schema.org/draft-07/schema#",
98+
type: "string",
99+
pattern: "^[a-zA-Z0-9]+$"
100+
};
101+
102+
const zodSchema = convertJsonSchemaToZod(jsonSchema);
103+
const resultSchema = zodToJsonSchema(zodSchema);
104+
expect(resultSchema).toEqual(jsonSchema);
105+
});
106+
});
107+
108+
describe("Number validation", () => {
109+
it("should support minimum and maximum constraints", () => {
110+
const jsonSchema = {
111+
$schema: "http://json-schema.org/draft-07/schema#",
112+
type: "number",
113+
minimum: 0,
114+
maximum: 100
115+
};
116+
117+
const zodSchema = convertJsonSchemaToZod(jsonSchema);
118+
const resultSchema = zodToJsonSchema(zodSchema);
119+
expect(resultSchema).toEqual(jsonSchema);
120+
});
121+
122+
it("should support exclusiveMinimum and exclusiveMaximum constraints", () => {
123+
const jsonSchema = {
124+
$schema: "http://json-schema.org/draft-07/schema#",
125+
type: "number",
126+
exclusiveMinimum: 0,
127+
exclusiveMaximum: 100
128+
};
129+
130+
const zodSchema = convertJsonSchemaToZod(jsonSchema);
131+
const resultSchema = zodToJsonSchema(zodSchema);
132+
expect(resultSchema).toEqual(jsonSchema);
133+
});
134+
135+
it("should support multipleOf constraint", () => {
136+
const jsonSchema = {
137+
$schema: "http://json-schema.org/draft-07/schema#",
138+
type: "number",
139+
multipleOf: 5
140+
};
141+
142+
const zodSchema = convertJsonSchemaToZod(jsonSchema);
143+
const resultSchema = zodToJsonSchema(zodSchema);
144+
expect(resultSchema).toEqual(jsonSchema);
145+
});
146+
});
147+
148+
describe("Array validation", () => {
149+
it("should support minItems and maxItems constraints", () => {
150+
const jsonSchema = {
151+
$schema: "http://json-schema.org/draft-07/schema#",
152+
type: "array",
153+
items: {
154+
type: "string"
155+
},
156+
minItems: 1,
157+
maxItems: 10
158+
};
159+
160+
const zodSchema = convertJsonSchemaToZod(jsonSchema);
161+
const resultSchema = zodToJsonSchema(zodSchema);
162+
expect(resultSchema).toEqual(jsonSchema);
163+
});
164+
165+
it("should support uniqueItems constraint", () => {
166+
const jsonSchema = {
167+
$schema: "http://json-schema.org/draft-07/schema#",
168+
type: "array",
169+
items: {
170+
type: "string"
171+
},
172+
uniqueItems: true
173+
};
174+
175+
const zodSchema = convertJsonSchemaToZod(jsonSchema);
176+
177+
// Unfortunately, zod-to-json-schema doesn't properly translate refinements for uniqueItems
178+
// So we'll verify the functionality by testing with actual data
179+
expect(() => zodSchema.parse(["a", "b", "c"])).not.toThrow();
180+
expect(() => zodSchema.parse(["a", "a", "c"])).toThrow();
181+
182+
// We can't do the normal round-trip test, so we'll verify key parts of the schema
183+
const resultSchema = zodToJsonSchema(zodSchema);
184+
expect(resultSchema.type).toEqual("array");
185+
expect(resultSchema.items).toEqual({ type: "string" });
186+
});
187+
});
188+
});

src/index.ts

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,46 @@ export function convertJsonSchemaToZod(schema: any): z.ZodTypeAny {
3333
// Handle primitive types
3434
if (schema.type) {
3535
switch (schema.type) {
36-
case "string":
37-
return addMetadata(z.string(), schema);
36+
case "string": {
37+
let stringSchema = z.string();
38+
39+
// Apply string-specific constraints
40+
if (schema.minLength !== undefined) {
41+
stringSchema = stringSchema.min(schema.minLength);
42+
}
43+
if (schema.maxLength !== undefined) {
44+
stringSchema = stringSchema.max(schema.maxLength);
45+
}
46+
if (schema.pattern !== undefined) {
47+
const regex = new RegExp(schema.pattern);
48+
stringSchema = stringSchema.regex(regex);
49+
}
50+
51+
return addMetadata(stringSchema, schema);
52+
}
3853
case "number":
39-
return addMetadata(z.number(), schema);
40-
case "integer":
41-
return addMetadata(z.number().int(), schema);
54+
case "integer": {
55+
let numberSchema = schema.type === "integer" ? z.number().int() : z.number();
56+
57+
// Apply number-specific constraints
58+
if (schema.minimum !== undefined) {
59+
numberSchema = numberSchema.min(schema.minimum);
60+
}
61+
if (schema.maximum !== undefined) {
62+
numberSchema = numberSchema.max(schema.maximum);
63+
}
64+
if (schema.exclusiveMinimum !== undefined) {
65+
numberSchema = numberSchema.gt(schema.exclusiveMinimum);
66+
}
67+
if (schema.exclusiveMaximum !== undefined) {
68+
numberSchema = numberSchema.lt(schema.exclusiveMaximum);
69+
}
70+
if (schema.multipleOf !== undefined) {
71+
numberSchema = numberSchema.multipleOf(schema.multipleOf);
72+
}
73+
74+
return addMetadata(numberSchema, schema);
75+
}
4276
case "boolean":
4377
return addMetadata(z.boolean(), schema);
4478
case "null":
@@ -77,14 +111,48 @@ export function convertJsonSchemaToZod(schema: any): z.ZodTypeAny {
77111
return addMetadata(zodSchema, schema);
78112
}
79113
return addMetadata(z.object({}), schema);
80-
case "array":
114+
case "array": {
81115
let arraySchema;
82116
if (schema.items) {
83117
arraySchema = z.array(convertJsonSchemaToZod(schema.items));
84118
} else {
85119
arraySchema = z.array(z.any());
86120
}
121+
122+
// Apply array-specific constraints
123+
if (schema.minItems !== undefined) {
124+
arraySchema = arraySchema.min(schema.minItems);
125+
}
126+
if (schema.maxItems !== undefined) {
127+
arraySchema = arraySchema.max(schema.maxItems);
128+
}
129+
if (schema.uniqueItems === true) {
130+
// To enforce uniqueness, we need a custom refine function
131+
// that checks if all elements are unique
132+
arraySchema = arraySchema.refine(
133+
(items) => {
134+
const seen = new Set();
135+
return items.every((item) => {
136+
// For primitive values, we can use a Set directly
137+
if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') {
138+
if (seen.has(item)) return false;
139+
seen.add(item);
140+
return true;
141+
}
142+
// For objects, we'd need more complex comparison
143+
// For simplicity, we stringfy objects for comparison
144+
const serialized = JSON.stringify(item);
145+
if (seen.has(serialized)) return false;
146+
seen.add(serialized);
147+
return true;
148+
});
149+
},
150+
{ message: "Array items must be unique" }
151+
);
152+
}
153+
87154
return addMetadata(arraySchema, schema);
155+
}
88156
}
89157
}
90158

0 commit comments

Comments
 (0)