Skip to content

Commit ea94da9

Browse files
authored
Merge pull request #15 from glideapps/required-in-raw-shape
Respect `required` in `jsonSchemaObjectToZodRawShape`
2 parents 4c9f82f + a9fad8d commit ea94da9

File tree

2 files changed

+129
-22
lines changed

2 files changed

+129
-22
lines changed

src/index.test.ts

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,10 +1022,14 @@ describe("jsonSchemaObjectToZodRawShape", () => {
10221022
expect(rawShape).toHaveProperty("age");
10231023
expect(rawShape).toHaveProperty("isActive");
10241024

1025-
// Verify types are correct
1025+
// Verify types are correct - required fields are direct types
10261026
expect(rawShape.name instanceof z.ZodString).toBe(true);
10271027
expect(rawShape.age instanceof z.ZodNumber).toBe(true);
1028-
expect(rawShape.isActive instanceof z.ZodBoolean).toBe(true);
1028+
1029+
// isActive is not in required array, so it should be optional
1030+
expect(rawShape.isActive instanceof z.ZodOptional).toBe(true);
1031+
// Check the inner type of the optional
1032+
expect((rawShape.isActive as z.ZodOptional<any>)._def.innerType instanceof z.ZodBoolean).toBe(true);
10291033
});
10301034

10311035
it("should handle empty properties", () => {
@@ -1072,7 +1076,10 @@ describe("jsonSchemaObjectToZodRawShape", () => {
10721076
const rawShape = jsonSchemaObjectToZodRawShape(jsonSchema);
10731077

10741078
expect(rawShape).toHaveProperty("user");
1075-
expect(rawShape.user instanceof z.ZodObject).toBe(true);
1079+
// Since there's no required array at the top level, user field is optional
1080+
expect(rawShape.user instanceof z.ZodOptional).toBe(true);
1081+
// The inner type should be a ZodObject
1082+
expect((rawShape.user as z.ZodOptional<any>)._def.innerType instanceof z.ZodObject).toBe(true);
10761083

10771084
// Create a schema with the raw shape to test validation
10781085
const schema = z.object(rawShape);
@@ -1090,6 +1097,11 @@ describe("jsonSchemaObjectToZodRawShape", () => {
10901097
user: { email: "[email protected]" },
10911098
}),
10921099
).toThrow();
1100+
1101+
// Since user is optional at the top level, empty object should pass
1102+
expect(() =>
1103+
schema.parse({}),
1104+
).not.toThrow();
10931105
});
10941106

10951107
it("should be usable to build custom schemas", () => {
@@ -1136,4 +1148,73 @@ describe("jsonSchemaObjectToZodRawShape", () => {
11361148
// Refinement with correct age, using same format as above test to confirm error was throw for correct reason
11371149
expect(() => customSchema.parse({ age: 19 })).not.toThrow();
11381150
});
1151+
1152+
it("should respect the required field when converting object properties", () => {
1153+
const jsonSchema = {
1154+
type: "object",
1155+
properties: {
1156+
requiredField: { type: "string" },
1157+
optionalField: { type: "number" },
1158+
anotherRequired: { type: "boolean" },
1159+
},
1160+
required: ["requiredField", "anotherRequired"],
1161+
};
1162+
1163+
const rawShape = jsonSchemaObjectToZodRawShape(jsonSchema);
1164+
1165+
// Create a schema to test the shape
1166+
const schema = z.object(rawShape);
1167+
1168+
// Required fields should be required
1169+
expect(() =>
1170+
schema.parse({
1171+
optionalField: 42,
1172+
}),
1173+
).toThrow(); // Missing required fields
1174+
1175+
// Optional field should be optional
1176+
expect(() =>
1177+
schema.parse({
1178+
requiredField: "test",
1179+
anotherRequired: true,
1180+
}),
1181+
).not.toThrow(); // Optional field missing is OK
1182+
1183+
// All fields present should work
1184+
expect(() =>
1185+
schema.parse({
1186+
requiredField: "test",
1187+
optionalField: 42,
1188+
anotherRequired: true,
1189+
}),
1190+
).not.toThrow();
1191+
});
1192+
1193+
it("should make all fields optional when required array is not present", () => {
1194+
const jsonSchema = {
1195+
type: "object",
1196+
properties: {
1197+
field1: { type: "string" },
1198+
field2: { type: "number" },
1199+
field3: { type: "boolean" },
1200+
},
1201+
// No required field - all properties should be optional
1202+
};
1203+
1204+
const rawShape = jsonSchemaObjectToZodRawShape(jsonSchema);
1205+
const schema = z.object(rawShape);
1206+
1207+
// All fields should be optional
1208+
expect(() => schema.parse({})).not.toThrow();
1209+
expect(() => schema.parse({ field1: "test" })).not.toThrow();
1210+
expect(() => schema.parse({ field2: 42 })).not.toThrow();
1211+
expect(() => schema.parse({ field3: true })).not.toThrow();
1212+
expect(() =>
1213+
schema.parse({
1214+
field1: "test",
1215+
field2: 42,
1216+
field3: true,
1217+
}),
1218+
).not.toThrow();
1219+
});
11391220
});

src/index.ts

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,39 @@ export { convertJsonSchemaToZod };
99
export { createUniqueItemsValidator, isValidWithSchema } from "./core/utils";
1010

1111
// Helper type to infer Zod types from JSON Schema properties
12-
type InferZodTypeFromJsonSchema<T> =
13-
T extends { type: "string" } ? z.ZodString :
14-
T extends { type: "number" } ? z.ZodNumber :
15-
T extends { type: "integer" } ? z.ZodNumber :
16-
T extends { type: "boolean" } ? z.ZodBoolean :
17-
T extends { type: "null" } ? z.ZodNull :
18-
T extends { type: "array" } ? z.ZodArray<z.ZodTypeAny> :
19-
T extends { type: "object" } ? z.ZodObject<any> :
20-
T extends { const: any } ? z.ZodLiteral<T["const"]> :
21-
T extends { enum: readonly any[] } ? z.ZodEnum<any> :
22-
z.ZodTypeAny;
12+
type InferZodTypeFromJsonSchema<T> = T extends { type: "string" }
13+
? z.ZodString
14+
: T extends { type: "number" }
15+
? z.ZodNumber
16+
: T extends { type: "integer" }
17+
? z.ZodNumber
18+
: T extends { type: "boolean" }
19+
? z.ZodBoolean
20+
: T extends { type: "null" }
21+
? z.ZodNull
22+
: T extends { type: "array" }
23+
? z.ZodArray<z.ZodTypeAny>
24+
: T extends { type: "object" }
25+
? z.ZodObject<any>
26+
: T extends { const: any }
27+
? z.ZodLiteral<T["const"]>
28+
: T extends { enum: readonly any[] }
29+
? z.ZodEnum<any>
30+
: z.ZodTypeAny;
2331

2432
// Helper type to map JSON Schema properties to Zod raw shape
2533
type InferZodRawShape<T extends Record<string, any>> = {
26-
[K in keyof T]: InferZodTypeFromJsonSchema<T[K]>
34+
[K in keyof T]: InferZodTypeFromJsonSchema<T[K]>;
2735
};
2836

2937
/**
3038
* Converts a JSON Schema object to a Zod raw shape with proper typing
3139
* @param schema The JSON Schema object to convert
3240
* @returns A Zod raw shape for use with z.object() with inferred types
3341
*/
34-
export function jsonSchemaObjectToZodRawShape<
35-
T extends JSONSchema.Schema & { properties: Record<string, any> }
36-
>(schema: T): InferZodRawShape<T["properties"]>;
42+
export function jsonSchemaObjectToZodRawShape<T extends JSONSchema.Schema & { properties: Record<string, any> }>(
43+
schema: T,
44+
): InferZodRawShape<T["properties"]>;
3745

3846
/**
3947
* Converts a JSON Schema object to a Zod raw shape
@@ -43,11 +51,29 @@ export function jsonSchemaObjectToZodRawShape<
4351
export function jsonSchemaObjectToZodRawShape(schema: JSONSchema.Schema): Record<string, z.ZodTypeAny>;
4452

4553
export function jsonSchemaObjectToZodRawShape(schema: JSONSchema.Schema): Record<string, z.ZodTypeAny> {
46-
const raw: Record<string, z.ZodTypeAny> = {};
47-
54+
const raw: Record<string, z.ZodType> = {};
55+
56+
// Get the required fields set for efficient lookup
57+
const requiredArray = Array.isArray(schema.required) ? schema.required : [];
58+
const requiredFields = new Set<string>(requiredArray);
59+
4860
for (const [key, value] of Object.entries(schema.properties ?? {})) {
4961
if (value === undefined) continue;
50-
raw[key] = convertJsonSchemaToZod(value);
62+
63+
let zodType = convertJsonSchemaToZod(value);
64+
65+
// If there's a required array and the field is not in it, make it optional
66+
// If there's no required array, all fields are optional by default in JSON Schema
67+
if (requiredArray.length > 0) {
68+
if (!requiredFields.has(key)) {
69+
zodType = zodType.optional();
70+
}
71+
} else {
72+
// No required array means all fields are optional
73+
zodType = zodType.optional();
74+
}
75+
76+
raw[key] = zodType;
5177
}
5278
return raw;
53-
}
79+
}

0 commit comments

Comments
 (0)