@signe/schema-to-zod 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@signe/schema-to-zod",
3
+ "version": "1.4.0",
4
+ "description": "",
5
+ "main": "./dist/index.js",
6
+ "scripts": {
7
+ "build": "tsup src/index.ts",
8
+ "dev": "tsup src/index.ts --watch"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ },
15
+ "./*": "./*"
16
+ },
17
+ "keywords": [],
18
+ "author": "Samuel Ronce",
19
+ "license": "MIT",
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "type": "module",
24
+ "dependencies": {
25
+ "@types/json-schema": "^7.0.15"
26
+ }
27
+ }
package/readme.md ADDED
@@ -0,0 +1,150 @@
1
+ # Schema to Zod
2
+
3
+ Convert JSON Schema to Zod validation schemas.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @signe/schema-to-zod
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { jsonSchemaToZod } from '@signe/schema-to-zod';
15
+ import { z } from 'zod';
16
+
17
+ // Define your JSON Schema
18
+ const schema = {
19
+ type: 'object',
20
+ properties: {
21
+ name: { type: 'string', minLength: 3 },
22
+ email: { type: 'string', format: 'email' },
23
+ age: { type: 'integer', minimum: 18 },
24
+ tags: {
25
+ type: 'array',
26
+ items: { type: 'string' },
27
+ minItems: 1
28
+ }
29
+ },
30
+ required: ['name', 'email']
31
+ };
32
+
33
+ // Convert to Zod schema
34
+ const zodSchema = z.object(jsonSchemaToZod(schema));
35
+
36
+ // Use the schema for validation
37
+ const result = zodSchema.safeParse({
38
+ name: 'John',
39
+ email: 'john@example.com',
40
+ age: 25,
41
+ tags: ['developer']
42
+ });
43
+
44
+ if (result.success) {
45
+ console.log('Valid data:', result.data);
46
+ } else {
47
+ console.log('Validation errors:', result.error);
48
+ }
49
+ ```
50
+
51
+ ## Features
52
+
53
+ - Supports common JSON Schema types:
54
+ - string
55
+ - number
56
+ - integer
57
+ - boolean
58
+ - array
59
+ - object
60
+ - null
61
+
62
+ - Handles JSON Schema formats:
63
+ - date
64
+ - date-time
65
+ - email
66
+ - hostname
67
+ - ipv4
68
+ - ipv6
69
+ - uri
70
+ - uuid
71
+ - color
72
+ - password
73
+ - code
74
+ - percent
75
+
76
+ - Supports validation rules:
77
+ - required fields
78
+ - string: minLength, maxLength, pattern
79
+ - number: minimum, maximum
80
+ - array: minItems, maxItems
81
+ - enum values
82
+
83
+ ## API
84
+
85
+ ### `jsonSchemaToZod(schema: JSONSchema7 | JSONSchema7Definition[]): Record<string, ZodType<unknown>>`
86
+
87
+ Converts a JSON Schema to a Zod schema object.
88
+
89
+ Parameters:
90
+ - `schema`: A JSON Schema object or array of schema objects
91
+
92
+ Returns:
93
+ - A record of Zod type definitions that can be used with `z.object()`
94
+
95
+ ## Examples
96
+
97
+ ### Nested Objects
98
+
99
+ ```typescript
100
+ const schema = {
101
+ type: 'object',
102
+ properties: {
103
+ user: {
104
+ type: 'object',
105
+ properties: {
106
+ profile: {
107
+ type: 'object',
108
+ properties: {
109
+ name: { type: 'string' },
110
+ age: { type: 'integer' }
111
+ },
112
+ required: ['name']
113
+ }
114
+ }
115
+ }
116
+ }
117
+ };
118
+
119
+ const zodSchema = z.object(jsonSchemaToZod(schema));
120
+ ```
121
+
122
+ ### Array Validation
123
+
124
+ ```typescript
125
+ const schema = {
126
+ type: 'object',
127
+ properties: {
128
+ users: {
129
+ type: 'array',
130
+ items: {
131
+ type: 'object',
132
+ properties: {
133
+ name: { type: 'string' },
134
+ age: { type: 'integer' }
135
+ },
136
+ required: ['name']
137
+ },
138
+ minItems: 1,
139
+ maxItems: 3
140
+ }
141
+ },
142
+ required: ['users']
143
+ };
144
+
145
+ const zodSchema = z.object(jsonSchemaToZod(schema));
146
+ ```
147
+
148
+ ## License
149
+
150
+ MIT
package/src/index.ts ADDED
@@ -0,0 +1,207 @@
1
+ import { z, ZodType, ZodTypeDef } from "zod";
2
+ import type { JSONSchema7, JSONSchema7Definition, JSONSchema7TypeName } from "json-schema";
3
+
4
+ /**
5
+ * Standard JSON Schema types only
6
+ */
7
+ type SchemaType = Exclude<JSONSchema7TypeName, 'array' | 'object'> | 'array' | 'object';
8
+
9
+ function isValidSchemaType(type: unknown): type is SchemaType {
10
+ if (typeof type !== 'string') return false;
11
+ return ['string', 'number', 'boolean', 'integer', 'array', 'object', 'null'].includes(type);
12
+ }
13
+
14
+ function isJSONSchema7(schema: JSONSchema7Definition): schema is JSONSchema7 {
15
+ return typeof schema !== 'boolean';
16
+ }
17
+
18
+ /**
19
+ * Zod schema for percentage values (0-100)
20
+ */
21
+ const percent = z.number().min(0).max(100);
22
+
23
+ /**
24
+ * Map of JSON Schema formats to their corresponding Zod schema creators
25
+ */
26
+ const formatMap: Record<string, (schema: JSONSchema7) => ZodType<unknown>> = {
27
+ 'date': () => z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
28
+ 'date-time': () => z.string().datetime(),
29
+ 'email': () => z.string().email(),
30
+ 'hostname': () => z.string(),
31
+ 'ipv4': () => z.string().ip({ version: 'v4' }),
32
+ 'ipv6': () => z.string().ip({ version: 'v6' }),
33
+ 'uri': () => z.string().url(),
34
+ 'uuid': () => z.string().uuid(),
35
+ 'color': () => z.string().regex(/^#(?:[0-9a-fA-F]{3}){1,2}$/),
36
+ 'password': () => z.string(),
37
+ 'code': () => z.string(),
38
+ 'percent': () => percent,
39
+ };
40
+
41
+ /**
42
+ * Map of JSON Schema types to their corresponding Zod schema creators
43
+ */
44
+ const typeMap: Record<SchemaType, (schema: JSONSchema7) => ZodType<unknown, ZodTypeDef, unknown>> = {
45
+ 'string': (schema) => {
46
+ if (schema.format && formatMap[schema.format]) {
47
+ return formatMap[schema.format](schema);
48
+ }
49
+ return z.string();
50
+ },
51
+ 'number': (schema) => {
52
+ if (schema.format === 'percent') {
53
+ return percent;
54
+ }
55
+ return z.number();
56
+ },
57
+ 'integer': () => z.number().int(),
58
+ 'boolean': () => z.boolean(),
59
+ 'null': () => z.null(),
60
+ 'array': (schema: JSONSchema7) => {
61
+ if (!schema.items || Array.isArray(schema.items)) {
62
+ throw new Error('Invalid array items');
63
+ }
64
+
65
+ if (!isJSONSchema7(schema.items)) {
66
+ throw new Error('Boolean schema is not supported for array items');
67
+ }
68
+
69
+ if (schema.items.type === 'object') {
70
+ return z.array(z.object(jsonSchemaToZod(schema.items)));
71
+ }
72
+
73
+ const itemType = schema.items.type;
74
+ if (!itemType || !isValidSchemaType(itemType)) {
75
+ throw new Error(`Unsupported array item type: ${itemType}`);
76
+ }
77
+
78
+ return z.array(typeMap[itemType](schema.items));
79
+ },
80
+ 'object': (schema: JSONSchema7) => {
81
+ if (!schema.properties) {
82
+ throw new Error('Invalid object schema: missing properties');
83
+ }
84
+ return z.object(jsonSchemaToZod(schema));
85
+ }
86
+ };
87
+
88
+ /**
89
+ * Applies validators to a Zod schema based on JSON Schema property constraints
90
+ */
91
+ function applyValidators(
92
+ zodType: ZodType<unknown>,
93
+ schema: JSONSchema7,
94
+ required: boolean
95
+ ): ZodType<unknown> {
96
+ let zodTypeWithValidators = zodType;
97
+
98
+ // String validators
99
+ if (schema.type === 'string' && required) {
100
+ zodTypeWithValidators = (zodTypeWithValidators as z.ZodString).min(1);
101
+
102
+ if (schema.minLength !== undefined) {
103
+ zodTypeWithValidators = (zodTypeWithValidators as z.ZodString).min(schema.minLength);
104
+ }
105
+
106
+ if (schema.maxLength !== undefined) {
107
+ zodTypeWithValidators = (zodTypeWithValidators as z.ZodString).max(schema.maxLength);
108
+ }
109
+ }
110
+
111
+ // Array validators
112
+ if (schema.type === 'array') {
113
+ if (schema.minItems !== undefined) {
114
+ zodTypeWithValidators = (zodTypeWithValidators as z.ZodArray<ZodType>).min(schema.minItems);
115
+ }
116
+ if (schema.maxItems !== undefined) {
117
+ zodTypeWithValidators = (zodTypeWithValidators as z.ZodArray<ZodType>).max(schema.maxItems);
118
+ }
119
+ }
120
+
121
+ // Number validators
122
+ if (schema.type === 'number' || schema.type === 'integer') {
123
+ if (schema.minimum !== undefined) {
124
+ zodTypeWithValidators = (zodTypeWithValidators as z.ZodNumber).min(schema.minimum);
125
+ }
126
+
127
+ if (schema.maximum !== undefined) {
128
+ zodTypeWithValidators = (zodTypeWithValidators as z.ZodNumber).max(schema.maximum);
129
+ }
130
+ }
131
+
132
+ // Enum validators
133
+ if (schema.enum) {
134
+ zodTypeWithValidators = zodTypeWithValidators.refine(
135
+ (value): value is typeof schema.enum[number] => schema.enum!.includes(value as string),
136
+ {
137
+ message: `Must be one of: ${schema.enum.join(", ")}`,
138
+ }
139
+ );
140
+ }
141
+
142
+ // Pattern validators
143
+ if (schema.pattern) {
144
+ zodTypeWithValidators = (zodTypeWithValidators as z.ZodString).regex(new RegExp(schema.pattern));
145
+ }
146
+
147
+ return zodTypeWithValidators;
148
+ }
149
+
150
+ function getTypeFunction(schema: JSONSchema7): ZodType<unknown> {
151
+ if (!schema.type || !isValidSchemaType(schema.type as string)) {
152
+ throw new Error(`Unsupported type: ${schema.type}`);
153
+ }
154
+ return typeMap[schema.type as SchemaType](schema);
155
+ }
156
+
157
+ /**
158
+ * Converts a JSON Schema property to a Zod schema
159
+ */
160
+ function convertPropertyToZod(
161
+ schema: JSONSchema7,
162
+ key: string,
163
+ parentSchema: JSONSchema7
164
+ ): ZodType<unknown> {
165
+ if (schema.$ref) {
166
+ return z.array(
167
+ z.object({
168
+ value: z.object({
169
+ id: z.string()
170
+ })
171
+ })
172
+ );
173
+ }
174
+
175
+ if (schema.type === 'object') {
176
+ return z.object(jsonSchemaToZod(schema));
177
+ }
178
+
179
+ const required = Array.isArray(parentSchema.required) && parentSchema.required.includes(key);
180
+ const typeValidator = applyValidators(getTypeFunction(schema), schema, required);
181
+ return required ? typeValidator : typeValidator.optional();
182
+ }
183
+
184
+ /**
185
+ * Converts a JSON Schema to a Zod schema
186
+ */
187
+ export function jsonSchemaToZod(
188
+ schema: JSONSchema7 | JSONSchema7Definition[],
189
+ ): Record<string, ZodType<unknown>> {
190
+ const zodSchema: Record<string, ZodType<unknown>> = {};
191
+
192
+ if (Array.isArray(schema)) {
193
+ // Handle array of schemas (merge all schemas)
194
+ for (const item of schema) {
195
+ if (!isJSONSchema7(item)) continue;
196
+ Object.assign(zodSchema, jsonSchemaToZod(item));
197
+ }
198
+ } else if (schema.type === 'object' && schema.properties) {
199
+ // Handle object schema
200
+ for (const [key, prop] of Object.entries(schema.properties)) {
201
+ if (!isJSONSchema7(prop)) continue;
202
+ zodSchema[key] = convertPropertyToZod(prop, key, schema);
203
+ }
204
+ }
205
+
206
+ return zodSchema;
207
+ }
@@ -0,0 +1,302 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest'
2
+ import { jsonSchemaToZod } from '../src'
3
+ import { z } from 'zod'
4
+
5
+ describe('jsonSchemaToZod', () => {
6
+
7
+ test('Should convert basic types correctly', async () => {
8
+ const schema = {
9
+ type: 'object',
10
+ properties: {
11
+ name: { type: 'string' },
12
+ email: { type: 'string', format: 'email' },
13
+ age: { type: 'integer' },
14
+ isActive: { type: 'boolean' },
15
+ scores: { type: 'array', items: { type: 'number' } },
16
+ },
17
+ required: ['name', 'email']
18
+ }
19
+
20
+ const zodSchema = jsonSchemaToZod(schema)
21
+
22
+ expect(Object.keys(zodSchema)).toEqual(['name', 'email', 'age', 'isActive', 'scores'])
23
+ })
24
+
25
+ test('Should throw an error for unsupported types', async () => {
26
+ const schema = {
27
+ type: 'object',
28
+ properties: {
29
+ unsupportedProp: { type: 'unsupported' }
30
+ }
31
+ }
32
+
33
+ expect(() => jsonSchemaToZod(schema)).toThrow('Unsupported type: unsupported')
34
+ })
35
+
36
+ test('Should apply validators correctly', async () => {
37
+ const schema = {
38
+ type: 'object',
39
+ properties: {
40
+ name: { type: 'string', minLength: 3, maxLength: 50 },
41
+ age: { type: 'integer', minimum: 18, maximum: 100 },
42
+ },
43
+ required: ['name']
44
+ }
45
+
46
+ const zodSchema = z.object(jsonSchemaToZod(schema))
47
+
48
+ // Name is required
49
+ let result = zodSchema.safeParse({ age: 30 })
50
+ expect(result.success).toBeFalsy()
51
+
52
+ // Name is too short
53
+ result = zodSchema.safeParse({ name: 'Jo', age: 30 })
54
+ expect(result.success).toBeFalsy()
55
+
56
+ // Name is too long
57
+ result = zodSchema.safeParse({ name: 'J'.repeat(51), age: 30 })
58
+ expect(result.success).toBeFalsy()
59
+
60
+ // Age is too low
61
+ result = zodSchema.safeParse({ name: 'John', age: 17 })
62
+ expect(result.success).toBeFalsy()
63
+
64
+ // Age is too high
65
+ result = zodSchema.safeParse({ name: 'John', age: 101 })
66
+ expect(result.success).toBeFalsy()
67
+
68
+ // All validations pass
69
+ result = zodSchema.safeParse({ name: 'John', age: 30 })
70
+ expect(result.success).toBeTruthy()
71
+ })
72
+
73
+ test('Should handle string type correctly', async () => {
74
+ const schema = {
75
+ type: 'object',
76
+ properties: {
77
+ name: { type: 'string' },
78
+ },
79
+ }
80
+
81
+ const zodSchema = z.object(jsonSchemaToZod(schema))
82
+
83
+ // Name is not a string
84
+ let result = zodSchema.safeParse({ name: 123 })
85
+ expect(result.success).toBeFalsy()
86
+
87
+ // Name is a string
88
+ result = zodSchema.safeParse({ name: 'John' })
89
+ expect(result.success).toBeTruthy()
90
+ })
91
+
92
+ test('Should handle special types correctly', async () => {
93
+ const schema = {
94
+ type: 'object',
95
+ properties: {
96
+ colorCode: { type: 'string', format: 'color' },
97
+ secretCode: { type: 'string', format: 'code' },
98
+ userPassword: { type: 'string', format: 'password' },
99
+ percentage: { type: 'number', format: 'percent' },
100
+ createdAt: { type: 'string', format: 'date' },
101
+ description: { type: 'string' }
102
+ }
103
+ }
104
+
105
+ const zodSchema = z.object(jsonSchemaToZod(schema))
106
+
107
+ // Test color validation
108
+ expect(zodSchema.safeParse({ colorCode: '#123456' }).success).toBeTruthy()
109
+ expect(zodSchema.safeParse({ colorCode: '#abc' }).success).toBeTruthy()
110
+ expect(zodSchema.safeParse({ colorCode: 'invalid' }).success).toBeFalsy()
111
+
112
+ // Test percent validation
113
+ expect(zodSchema.safeParse({ percentage: 50 }).success).toBeTruthy()
114
+ expect(zodSchema.safeParse({ percentage: 101 }).success).toBeFalsy()
115
+ expect(zodSchema.safeParse({ percentage: -1 }).success).toBeFalsy()
116
+ })
117
+
118
+ test('Should handle nested objects correctly', async () => {
119
+ const schema = {
120
+ type: 'object' as const,
121
+ properties: {
122
+ user: {
123
+ type: 'object' as const,
124
+ properties: {
125
+ profile: {
126
+ type: 'object' as const,
127
+ properties: {
128
+ name: { type: 'string' as const },
129
+ age: { type: 'integer' as const }
130
+ },
131
+ required: ['name']
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ const zodSchema = z.object(jsonSchemaToZod(schema))
139
+
140
+ expect(zodSchema.safeParse({
141
+ user: {
142
+ profile: {
143
+ name: 'John',
144
+ age: 30
145
+ }
146
+ }
147
+ }).success).toBeTruthy()
148
+
149
+ expect(zodSchema.safeParse({
150
+ user: {
151
+ profile: {
152
+ age: 30
153
+ }
154
+ }
155
+ }).success).toBeFalsy()
156
+ })
157
+
158
+ test('Should handle array of schemas correctly', async () => {
159
+ const schemas = [
160
+ {
161
+ schema: {
162
+ type: 'object',
163
+ properties: {
164
+ name: { type: 'string' }
165
+ }
166
+ }
167
+ },
168
+ {
169
+ schema: {
170
+ type: 'object',
171
+ properties: {
172
+ age: { type: 'integer' }
173
+ }
174
+ }
175
+ }
176
+ ]
177
+
178
+ const zodSchema = z.object(jsonSchemaToZod(schemas))
179
+
180
+ expect(zodSchema.safeParse({
181
+ name: 'John',
182
+ age: 30
183
+ }).success).toBeTruthy()
184
+ })
185
+
186
+ test('Should handle $ref correctly', async () => {
187
+ const schema = {
188
+ type: 'object',
189
+ properties: {
190
+ categories: {
191
+ $ref: '#/definitions/CategoryList'
192
+ }
193
+ }
194
+ }
195
+
196
+ const zodSchema = z.object(jsonSchemaToZod(schema))
197
+
198
+ expect(zodSchema.safeParse({
199
+ categories: [
200
+ { value: { id: '123' } }
201
+ ]
202
+ }).success).toBeTruthy()
203
+
204
+ expect(zodSchema.safeParse({
205
+ categories: [
206
+ { value: { wrong: '123' } }
207
+ ]
208
+ }).success).toBeFalsy()
209
+ })
210
+
211
+ test('Should handle pattern and enum validations', async () => {
212
+ const schema = {
213
+ type: 'object',
214
+ properties: {
215
+ code: {
216
+ type: 'string',
217
+ pattern: '^[A-Z]{3}[0-9]{3}$'
218
+ },
219
+ status: {
220
+ type: 'string',
221
+ enum: ['active', 'inactive', 'pending']
222
+ }
223
+ }
224
+ }
225
+
226
+ const zodSchema = z.object(jsonSchemaToZod(schema))
227
+
228
+ // Test pattern validation
229
+ expect(zodSchema.safeParse({ code: 'ABC123' }).success).toBeTruthy()
230
+ expect(zodSchema.safeParse({ code: 'invalid' }).success).toBeFalsy()
231
+
232
+ // Test enum validation
233
+ expect(zodSchema.safeParse({ status: 'active' }).success).toBeTruthy()
234
+ expect(zodSchema.safeParse({ status: 'invalid' }).success).toBeFalsy()
235
+ })
236
+
237
+ test('Should handle array items with complex validation', async () => {
238
+ const schema = {
239
+ type: 'object',
240
+ properties: {
241
+ users: {
242
+ type: 'array',
243
+ items: {
244
+ type: 'object',
245
+ properties: {
246
+ name: { type: 'string' },
247
+ age: { type: 'integer' }
248
+ },
249
+ required: ['name']
250
+ },
251
+ minItems: 1,
252
+ maxItems: 3
253
+ }
254
+ },
255
+ required: ['users']
256
+ }
257
+
258
+ const zodSchema = z.object(jsonSchemaToZod(schema))
259
+
260
+ // Valid case
261
+ let result = zodSchema.safeParse({
262
+ users: [
263
+ { name: 'John', age: 30 },
264
+ { name: 'Jane', age: 25 }
265
+ ]
266
+ })
267
+ expect(result.success).toBeTruthy()
268
+
269
+ // Too many items
270
+ result = zodSchema.safeParse({
271
+ users: [
272
+ { name: 'John', age: 30 },
273
+ { name: 'Jane', age: 25 },
274
+ { name: 'Bob', age: 35 },
275
+ { name: 'Alice', age: 28 }
276
+ ]
277
+ })
278
+ expect(result.success).toBeFalsy()
279
+
280
+ // Missing required field in array item
281
+ result = zodSchema.safeParse({
282
+ users: [
283
+ { age: 30 }
284
+ ]
285
+ })
286
+ expect(result.success).toBeFalsy()
287
+
288
+ // Wrong type in array item
289
+ result = zodSchema.safeParse({
290
+ users: [
291
+ { name: 'John', age: 'thirty' }
292
+ ]
293
+ })
294
+ expect(result.success).toBeFalsy()
295
+
296
+ // Empty array (violates minItems)
297
+ result = zodSchema.safeParse({
298
+ users: []
299
+ })
300
+ expect(result.success).toBeFalsy()
301
+ })
302
+ })