Skip to content

Commit 3f912d8

Browse files
committed
Fix and test
1 parent 70e0ea7 commit 3f912d8

File tree

2 files changed

+176
-7
lines changed

2 files changed

+176
-7
lines changed

src/index.test.ts

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,39 @@ describe("convertJsonSchemaToZod", () => {
132132
expect(resultSchema.enum).toEqual(jsonSchema.enum);
133133
});
134134

135+
it("should correctly convert a schema with single item mixed enum (no type)", () => {
136+
const jsonSchema = {
137+
$schema: "http://json-schema.org/draft-07/schema#",
138+
enum: [42] // Single non-string value
139+
};
140+
141+
const zodSchema = convertJsonSchemaToZod(jsonSchema);
142+
143+
// Test validation
144+
expect(() => zodSchema.parse(42)).not.toThrow();
145+
expect(() => zodSchema.parse(43)).toThrow();
146+
expect(() => zodSchema.parse("42")).toThrow();
147+
148+
// Should be represented as a literal in the schema
149+
const resultSchema = zodToJsonSchema(zodSchema);
150+
expect(resultSchema.const).toEqual(42);
151+
});
152+
153+
it("should handle empty enum case (no type)", () => {
154+
const jsonSchema = {
155+
$schema: "http://json-schema.org/draft-07/schema#",
156+
enum: [] // Empty enum
157+
};
158+
159+
const zodSchema = convertJsonSchemaToZod(jsonSchema);
160+
161+
// Empty enum without type should match nothing (z.never)
162+
expect(() => zodSchema.parse(42)).toThrow();
163+
expect(() => zodSchema.parse("anything")).toThrow();
164+
expect(() => zodSchema.parse(null)).toThrow();
165+
expect(() => zodSchema.parse({})).toThrow();
166+
});
167+
135168
it("should correctly convert a schema with string type and enum", () => {
136169
const jsonSchema = {
137170
$schema: "http://json-schema.org/draft-07/schema#",
@@ -149,6 +182,22 @@ describe("convertJsonSchemaToZod", () => {
149182
expect(resultSchema.type).toEqual("string");
150183
expect(resultSchema.enum).toEqual(jsonSchema.enum);
151184
});
185+
186+
it("should handle empty string enum (with type)", () => {
187+
const jsonSchema = {
188+
$schema: "http://json-schema.org/draft-07/schema#",
189+
type: "string",
190+
enum: [] // Empty enum
191+
};
192+
193+
const zodSchema = convertJsonSchemaToZod(jsonSchema);
194+
195+
// Empty enum with string type should still validate as a string
196+
expect(() => zodSchema.parse("any string")).not.toThrow();
197+
198+
const resultSchema = zodToJsonSchema(zodSchema);
199+
expect(resultSchema.type).toEqual("string");
200+
});
152201

153202
it("should correctly convert a schema with number type and enum", () => {
154203
const jsonSchema = {
@@ -167,8 +216,25 @@ describe("convertJsonSchemaToZod", () => {
167216
expect(resultSchema.type).toEqual("number");
168217
expect(resultSchema.enum).toEqual(jsonSchema.enum);
169218
});
219+
220+
it("should correctly convert a schema with number type and single-item enum", () => {
221+
const jsonSchema = {
222+
$schema: "http://json-schema.org/draft-07/schema#",
223+
type: "number",
224+
enum: [42]
225+
};
226+
227+
const zodSchema = convertJsonSchemaToZod(jsonSchema);
228+
229+
// Test validation
230+
expect(() => zodSchema.parse(42)).not.toThrow();
231+
expect(() => zodSchema.parse(43)).toThrow();
232+
233+
const resultSchema = zodToJsonSchema(zodSchema);
234+
expect(resultSchema.const).toEqual(42);
235+
});
170236

171-
it("should correctly convert a schema with boolean type and enum", () => {
237+
it("should correctly convert a schema with boolean type and single-item enum", () => {
172238
const jsonSchema = {
173239
$schema: "http://json-schema.org/draft-07/schema#",
174240
type: "boolean",
@@ -181,9 +247,61 @@ describe("convertJsonSchemaToZod", () => {
181247
expect(() => zodSchema.parse(true)).not.toThrow();
182248
expect(() => zodSchema.parse(false)).toThrow();
183249

250+
// With our current implementation, single-item enums are converted to literals
251+
// so zod-to-json-schema might represent it differently than the original.
252+
// We only check that the schema correctly validates values, not its exact representation
253+
const resultSchema = zodToJsonSchema(zodSchema);
254+
expect(resultSchema.type).toEqual("boolean");
255+
});
256+
257+
it("should correctly convert a schema with boolean type and multiple-item enum", () => {
258+
const jsonSchema = {
259+
$schema: "http://json-schema.org/draft-07/schema#",
260+
type: "boolean",
261+
enum: [true, false]
262+
};
263+
264+
const zodSchema = convertJsonSchemaToZod(jsonSchema);
265+
266+
// Test validation - should accept both true and false since both are in the enum
267+
expect(() => zodSchema.parse(true)).not.toThrow();
268+
expect(() => zodSchema.parse(false)).not.toThrow();
269+
270+
const resultSchema = zodToJsonSchema(zodSchema);
271+
expect(resultSchema.type).toEqual("boolean");
272+
});
273+
274+
it("should handle empty number enum (with type)", () => {
275+
const jsonSchema = {
276+
$schema: "http://json-schema.org/draft-07/schema#",
277+
type: "number",
278+
enum: [] // Empty enum
279+
};
280+
281+
const zodSchema = convertJsonSchemaToZod(jsonSchema);
282+
283+
// Empty enum with type should still be a valid schema
284+
expect(() => zodSchema.parse(42)).not.toThrow();
285+
286+
const resultSchema = zodToJsonSchema(zodSchema);
287+
expect(resultSchema.type).toEqual("number");
288+
});
289+
290+
it("should handle empty boolean enum (with type)", () => {
291+
const jsonSchema = {
292+
$schema: "http://json-schema.org/draft-07/schema#",
293+
type: "boolean",
294+
enum: [] // Empty enum
295+
};
296+
297+
const zodSchema = convertJsonSchemaToZod(jsonSchema);
298+
299+
// Empty enum with type should still be a valid schema
300+
expect(() => zodSchema.parse(true)).not.toThrow();
301+
expect(() => zodSchema.parse(false)).not.toThrow();
302+
184303
const resultSchema = zodToJsonSchema(zodSchema);
185304
expect(resultSchema.type).toEqual("boolean");
186-
expect(resultSchema.enum).toEqual(jsonSchema.enum);
187305
});
188306
});
189307

src/index.ts

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ export function convertJsonSchemaToZod(schema: JSONSchema): z.ZodTypeAny {
6868
case "string": {
6969
// Handle enum first if it exists for string type
7070
if (schema.enum) {
71+
// Empty enum case, default to string type
72+
if (schema.enum.length === 0) {
73+
return addMetadata(z.string(), schema);
74+
}
75+
7176
// Since we know this is a string type, we can safely cast enum values
7277
return addMetadata(z.enum(schema.enum as [string, ...string[]]), schema);
7378
}
@@ -90,11 +95,26 @@ export function convertJsonSchemaToZod(schema: JSONSchema): z.ZodTypeAny {
9095
}
9196
case "number":
9297
case "integer": {
93-
// Handle enum first if it exists for number type
98+
// Handle enum if it exists for number type
9499
if (schema.enum) {
100+
// Empty enum case, default to number type
101+
if (schema.enum.length === 0) {
102+
return addMetadata(z.number(), schema);
103+
}
104+
95105
// For numbers we need a union of literals since z.enum only works with strings
96106
const options = schema.enum.map(val => z.literal(val as number));
97-
return addMetadata(z.union(options as [z.ZodLiteral<any>, z.ZodLiteral<any>, ...z.ZodLiteral<any>[]]), schema);
107+
108+
// Handle single option enum specially
109+
if (options.length === 1) {
110+
return addMetadata(options[0], schema);
111+
}
112+
113+
// For multiple options, create a union
114+
if (options.length >= 2) {
115+
const unionSchema = z.union([options[0], options[1], ...options.slice(2)]);
116+
return addMetadata(unionSchema, schema);
117+
}
98118
}
99119

100120
let numberSchema = schema.type === "integer" ? z.number().int() : z.number();
@@ -121,8 +141,23 @@ export function convertJsonSchemaToZod(schema: JSONSchema): z.ZodTypeAny {
121141
case "boolean":
122142
// Handle enum for boolean type if present
123143
if (schema.enum) {
144+
// Empty enum case, default to boolean type
145+
if (schema.enum.length === 0) {
146+
return addMetadata(z.boolean(), schema);
147+
}
148+
124149
const options = schema.enum.map(val => z.literal(val as boolean));
125-
return addMetadata(z.union(options as [z.ZodLiteral<any>, ...z.ZodLiteral<any>[]]), schema);
150+
151+
// Handle single option enum specially
152+
if (options.length === 1) {
153+
return addMetadata(options[0], schema);
154+
}
155+
156+
// For multiple options, create a union
157+
if (options.length >= 2) {
158+
const unionSchema = z.union([options[0], options[1], ...options.slice(2)]);
159+
return addMetadata(unionSchema, schema);
160+
}
126161
}
127162
return addMetadata(z.boolean(), schema);
128163
case "null":
@@ -210,6 +245,12 @@ export function convertJsonSchemaToZod(schema: JSONSchema): z.ZodTypeAny {
210245

211246
// Handle enum (when type is not specified)
212247
if (schema.enum) {
248+
// Empty enum case, default to never type for empty enum without type
249+
// This matches JSON Schema's behavior where an empty enum without type should match nothing
250+
if (schema.enum.length === 0) {
251+
return addMetadata(z.never(), schema);
252+
}
253+
213254
// Check if all enum values are strings
214255
const allStrings = schema.enum.every(val => typeof val === 'string');
215256

@@ -219,7 +260,17 @@ export function convertJsonSchemaToZod(schema: JSONSchema): z.ZodTypeAny {
219260
} else {
220261
// For mixed types or non-strings, use a union of literals
221262
const options = schema.enum.map(val => z.literal(val));
222-
return addMetadata(z.union(options as [z.ZodLiteral<any>, z.ZodLiteral<any>, ...z.ZodLiteral<any>[]]), schema);
263+
264+
// Handle single option enum specially
265+
if (options.length === 1) {
266+
return addMetadata(options[0], schema);
267+
}
268+
269+
// For multiple options, create a union
270+
if (options.length >= 2) {
271+
const unionSchema = z.union([options[0], options[1], ...options.slice(2)]);
272+
return addMetadata(unionSchema, schema);
273+
}
223274
}
224275
}
225276

@@ -266,4 +317,4 @@ export function jsonSchemaObjectToZodRawShape(schema: JSONSchema): z.ZodRawShape
266317
raw[key] = convertJsonSchemaToZod(value);
267318
}
268319
return raw;
269-
}
320+
}

0 commit comments

Comments
 (0)