Skip to content

Commit ba0b109

Browse files
committed
Only accept the default if it satisfied the schema, and document
1 parent 72b44d4 commit ba0b109

File tree

4 files changed

+77
-43
lines changed

4 files changed

+77
-43
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ This library provides comprehensive support for JSON Schema Draft 2020-12 featur
233233
### Additional Features
234234
- `title` - Schema titles (carried over to Zod schemas)
235235
- `description` - Schema descriptions (carried over to Zod schemas)
236+
- `default` - Default value annotation, but ignored if it doesn't conform to the schema
236237
- Boolean schemas (`true` = allow anything, `false` = allow nothing)
237238
- Implicit type detection from constraints
238239
- Comprehensive error messages

src/core/converter.ts

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,27 @@ import { PrimitiveHandler, RefinementHandler, TypeSchemas } from "./types";
66
import { TypeHandler } from "../handlers/primitive/type";
77
import { ConstHandler } from "../handlers/primitive/const";
88
import { EnumHandler } from "../handlers/primitive/enum";
9-
import { ImplicitStringHandler, MinLengthHandler, MaxLengthHandler, PatternHandler } from "../handlers/primitive/string";
10-
import { MinimumHandler, MaximumHandler, ExclusiveMinimumHandler, ExclusiveMaximumHandler, MultipleOfHandler } from "../handlers/primitive/number";
9+
import {
10+
ImplicitStringHandler,
11+
MinLengthHandler,
12+
MaxLengthHandler,
13+
PatternHandler,
14+
} from "../handlers/primitive/string";
15+
import {
16+
MinimumHandler,
17+
MaximumHandler,
18+
ExclusiveMinimumHandler,
19+
ExclusiveMaximumHandler,
20+
MultipleOfHandler,
21+
} from "../handlers/primitive/number";
1122
import { ImplicitArrayHandler, MinItemsHandler, MaxItemsHandler, ItemsHandler } from "../handlers/primitive/array";
1223
import { TupleHandler } from "../handlers/primitive/tuple";
13-
import { PropertiesHandler, ImplicitObjectHandler, MaxPropertiesHandler, MinPropertiesHandler } from "../handlers/primitive/object";
24+
import {
25+
PropertiesHandler,
26+
ImplicitObjectHandler,
27+
MaxPropertiesHandler,
28+
MinPropertiesHandler,
29+
} from "../handlers/primitive/object";
1430

1531
// Import refinement handlers
1632
import { NotHandler } from "../handlers/refinement/not";
@@ -33,30 +49,30 @@ const primitiveHandlers: PrimitiveHandler[] = [
3349
new ConstHandler(),
3450
new EnumHandler(),
3551
new TypeHandler(),
36-
52+
3753
// Implicit type detection - must run before other constraints
3854
new ImplicitStringHandler(),
3955
new ImplicitArrayHandler(),
4056
new ImplicitObjectHandler(),
41-
57+
4258
// String constraints
4359
new MinLengthHandler(),
4460
new MaxLengthHandler(),
4561
new PatternHandler(),
46-
62+
4763
// Number constraints
4864
new MinimumHandler(),
4965
new MaximumHandler(),
5066
new ExclusiveMinimumHandler(),
5167
new ExclusiveMaximumHandler(),
5268
new MultipleOfHandler(),
53-
69+
5470
// Array constraints - TupleHandler must run before ItemsHandler
5571
new TupleHandler(),
5672
new MinItemsHandler(),
5773
new MaxItemsHandler(),
5874
new ItemsHandler(),
59-
75+
6076
// Object constraints
6177
new MaxPropertiesHandler(),
6278
new MinPropertiesHandler(),
@@ -68,24 +84,24 @@ const refinementHandlers: RefinementHandler[] = [
6884
new ProtoRequiredHandler(),
6985
new EnumComplexHandler(),
7086
new ConstComplexHandler(),
71-
new DefaultHandler(),
72-
87+
7388
// Logical combinations
7489
new AllOfHandler(),
7590
new AnyOfHandler(),
7691
new OneOfHandler(),
77-
92+
7893
// Type-specific refinements
7994
new PrefixItemsHandler(),
8095
new ObjectPropertiesHandler(),
81-
96+
8297
// Array refinements
8398
new ContainsHandler(),
84-
99+
85100
// Other refinements
86101
new NotHandler(),
87102
new UniqueItemsHandler(),
88-
103+
new DefaultHandler(),
104+
89105
// Metadata last
90106
new MetadataHandler(),
91107
];
@@ -101,14 +117,14 @@ export function convertJsonSchemaToZod(schema: JSONSchema.BaseSchema | boolean):
101117

102118
// Phase 1: Initialize type schemas and apply primitive handlers
103119
const types: TypeSchemas = {};
104-
120+
105121
for (const handler of primitiveHandlers) {
106122
handler.apply(types, schema);
107123
}
108-
124+
109125
// Build array of allowed type schemas
110126
const allowedSchemas: z.ZodTypeAny[] = [];
111-
127+
112128
if (types.string !== false) {
113129
allowedSchemas.push(types.string || z.string());
114130
}
@@ -134,12 +150,12 @@ export function convertJsonSchemaToZod(schema: JSONSchema.BaseSchema | boolean):
134150
} else {
135151
// Use custom validator that rejects arrays for default object schema
136152
const objectSchema = z.custom<object>((val) => {
137-
return typeof val === 'object' && val !== null && !Array.isArray(val);
153+
return typeof val === "object" && val !== null && !Array.isArray(val);
138154
}, "Must be an object, not an array");
139155
allowedSchemas.push(objectSchema);
140156
}
141157
}
142-
158+
143159
// Create base schema
144160
let zodSchema: z.ZodTypeAny;
145161
if (allowedSchemas.length === 0) {
@@ -148,22 +164,22 @@ export function convertJsonSchemaToZod(schema: JSONSchema.BaseSchema | boolean):
148164
zodSchema = allowedSchemas[0];
149165
} else {
150166
// Check if this is an unconstrained schema (all default types enabled)
151-
const hasConstraints = Object.keys(schema).some(key =>
152-
key !== '$schema' && key !== 'title' && key !== 'description'
167+
const hasConstraints = Object.keys(schema).some(
168+
(key) => key !== "$schema" && key !== "title" && key !== "description",
153169
);
154-
170+
155171
if (!hasConstraints) {
156172
// Empty schema with no constraints should be z.any()
157173
zodSchema = z.any();
158174
} else {
159175
zodSchema = z.union(allowedSchemas as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]]);
160176
}
161177
}
162-
178+
163179
// Phase 2: Apply refinement handlers
164180
for (const handler of refinementHandlers) {
165181
zodSchema = handler.apply(zodSchema, schema);
166182
}
167-
183+
168184
return zodSchema;
169-
}
185+
}

src/handlers/refinement/default.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { z } from "zod/v4";
22
import type { JSONSchema } from "zod/v4/core";
33
import { RefinementHandler } from "../../core/types";
4-
import { deepEqual } from "../../core/utils";
54

65
export class DefaultHandler implements RefinementHandler {
76
apply(zodSchema: z.ZodTypeAny, schema: JSONSchema.BaseSchema): z.ZodTypeAny {
8-
if (!schema.default) return zodSchema;
7+
const { default: v } = schema;
98

10-
return zodSchema.default(schema.default);
9+
if (v === undefined) return zodSchema;
10+
if (!zodSchema.safeParse(v).success) {
11+
// should we error-log here?
12+
return zodSchema;
13+
}
14+
15+
return zodSchema.default(v);
1116
}
12-
}
17+
}

src/index.test.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it, expect } from "vitest";
22
import { convertJsonSchemaToZod, jsonSchemaObjectToZodRawShape } from "./index";
33
import { z } from "zod/v4";
4-
import {JSONSchema} from "zod/v4/core";
4+
import { JSONSchema } from "zod/v4/core";
55

66
describe("convertJsonSchemaToZod", () => {
77
it("should correctly convert a schema with additionalProperties: {}", () => {
@@ -93,7 +93,11 @@ describe("convertJsonSchemaToZod", () => {
9393

9494
const resultSchema = z.toJSONSchema(zodSchema);
9595
// Zod v4 converts unions to anyOf instead of enum
96-
expect(resultSchema.anyOf).toEqual([{ type: "number", const: 1 }, { type: "number", const: 2 }, { type: "number", const: 3 }]);
96+
expect(resultSchema.anyOf).toEqual([
97+
{ type: "number", const: 1 },
98+
{ type: "number", const: 2 },
99+
{ type: "number", const: 3 },
100+
]);
97101
});
98102

99103
it("should correctly convert a schema with boolean enum (no type)", () => {
@@ -111,7 +115,10 @@ describe("convertJsonSchemaToZod", () => {
111115

112116
const resultSchema = z.toJSONSchema(zodSchema);
113117
// Zod v4 converts unions to anyOf instead of enum
114-
expect(resultSchema.anyOf).toEqual([{ type: "boolean", const: true }, { type: "boolean", const: false }]);
118+
expect(resultSchema.anyOf).toEqual([
119+
{ type: "boolean", const: true },
120+
{ type: "boolean", const: false },
121+
]);
115122
});
116123

117124
it("should correctly convert a schema with mixed enum (no type)", () => {
@@ -132,7 +139,12 @@ describe("convertJsonSchemaToZod", () => {
132139

133140
const resultSchema = z.toJSONSchema(zodSchema);
134141
// Zod v4 converts unions to anyOf instead of enum
135-
expect(resultSchema.anyOf).toEqual([{ type: "string", const: "red" }, { type: "number", const: 1 }, { type: "boolean", const: true }, { type: "null" }]);
142+
expect(resultSchema.anyOf).toEqual([
143+
{ type: "string", const: "red" },
144+
{ type: "number", const: 1 },
145+
{ type: "boolean", const: true },
146+
{ type: "null" },
147+
]);
136148
});
137149

138150
it("should correctly convert a schema with single item mixed enum (no type)", () => {
@@ -570,12 +582,12 @@ describe("convertJsonSchemaToZod", () => {
570582

571583
// Test that the validation works correctly
572584
expect(zodSchema.safeParse("hello").success).toBe(true); // 5 chars, within range
573-
expect(zodSchema.safeParse("hi").success).toBe(false); // 2 chars, too short
585+
expect(zodSchema.safeParse("hi").success).toBe(false); // 2 chars, too short
574586
expect(zodSchema.safeParse("this is too long").success).toBe(false); // too long
575587

576588
// Test Unicode support
577589
expect(zodSchema.safeParse("💩💩💩").success).toBe(true); // 3 graphemes
578-
expect(zodSchema.safeParse("💩").success).toBe(false); // 1 grapheme, too short
590+
expect(zodSchema.safeParse("💩").success).toBe(false); // 1 grapheme, too short
579591

580592
// Note: length constraints implemented with .refine() don't round-trip
581593
// back to JSON Schema, so we only test the validation behavior
@@ -1025,7 +1037,7 @@ describe("jsonSchemaObjectToZodRawShape", () => {
10251037
// Verify types are correct - required fields are direct types
10261038
expect(rawShape.name instanceof z.ZodString).toBe(true);
10271039
expect(rawShape.age instanceof z.ZodNumber).toBe(true);
1028-
1040+
10291041
// isActive is not in required array, so it should be optional
10301042
expect(rawShape.isActive instanceof z.ZodOptional).toBe(true);
10311043
// Check the inner type of the optional
@@ -1097,16 +1109,14 @@ describe("jsonSchemaObjectToZodRawShape", () => {
10971109
user: { email: "[email protected]" },
10981110
}),
10991111
).toThrow();
1100-
1112+
11011113
// Since user is optional at the top level, empty object should pass
1102-
expect(() =>
1103-
schema.parse({}),
1104-
).not.toThrow();
1114+
expect(() => schema.parse({})).not.toThrow();
11051115
});
11061116

11071117
it("should be usable to build custom schemas", () => {
11081118
const jsonSchema: JSONSchema.BaseSchema = {
1109-
type: "object",
1119+
type: "object",
11101120
properties: {
11111121
name: { type: "string" },
11121122
age: { type: "integer" },
@@ -1235,7 +1245,8 @@ describe("jsonSchemaObjectToZodRawShape", () => {
12351245
isActive: { type: "boolean" },
12361246
},
12371247
required: ["name", "age", "isActive"],
1238-
}
1248+
},
1249+
field6: { type: "string", default: 42 },
12391250
},
12401251
// No required field - all properties should be optional
12411252
};
@@ -1251,6 +1262,7 @@ describe("jsonSchemaObjectToZodRawShape", () => {
12511262
field3: true,
12521263
field4: "test2",
12531264
field5: { name: "test", age: 10, isActive: true },
1254-
})
1265+
// `field6` doesn't get the default, because it's not the correct type
1266+
});
12551267
});
12561268
});

0 commit comments

Comments
 (0)