@orpc/openapi 0.0.3 → 0.0.5
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/LICENSE +21 -0
- package/dist/index.js +64 -21
- package/dist/index.js.map +1 -1
- package/dist/src/generator.d.ts.map +1 -1
- package/dist/src/zod-to-json-schema.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -11
- package/src/generator.test.ts +186 -8
- package/src/generator.ts +102 -31
- package/src/zod-to-json-schema.test.ts +1 -1
- package/src/zod-to-json-schema.ts +22 -33
package/src/generator.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { OpenAPIObject } from 'openapi3-ts/oas31'
|
|
1
2
|
import { oc } from '@orpc/contract'
|
|
2
3
|
import { oz } from '@orpc/zod'
|
|
3
|
-
import type { OpenAPIObject } from 'openapi3-ts/oas31'
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
import { generateOpenAPI } from './generator'
|
|
6
6
|
|
|
@@ -36,7 +36,7 @@ it('works', () => {
|
|
|
36
36
|
'/ping': {
|
|
37
37
|
post: {
|
|
38
38
|
responses: {
|
|
39
|
-
|
|
39
|
+
200: {
|
|
40
40
|
description: 'OK',
|
|
41
41
|
content: {
|
|
42
42
|
'application/json': {
|
|
@@ -63,7 +63,7 @@ it('works', () => {
|
|
|
63
63
|
},
|
|
64
64
|
],
|
|
65
65
|
responses: {
|
|
66
|
-
|
|
66
|
+
200: {
|
|
67
67
|
description: 'OK',
|
|
68
68
|
content: {
|
|
69
69
|
'application/json': {
|
|
@@ -133,7 +133,7 @@ it('throwOnMissingTagDefinition option', () => {
|
|
|
133
133
|
'/ping': {
|
|
134
134
|
post: {
|
|
135
135
|
responses: {
|
|
136
|
-
|
|
136
|
+
200: {
|
|
137
137
|
description: 'OK',
|
|
138
138
|
content: {
|
|
139
139
|
'application/json': {
|
|
@@ -160,7 +160,7 @@ it('throwOnMissingTagDefinition option', () => {
|
|
|
160
160
|
},
|
|
161
161
|
],
|
|
162
162
|
responses: {
|
|
163
|
-
|
|
163
|
+
200: {
|
|
164
164
|
description: 'OK',
|
|
165
165
|
content: {
|
|
166
166
|
'application/json': {
|
|
@@ -237,7 +237,7 @@ it('support single file upload', () => {
|
|
|
237
237
|
},
|
|
238
238
|
},
|
|
239
239
|
responses: {
|
|
240
|
-
|
|
240
|
+
200: {
|
|
241
241
|
content: {
|
|
242
242
|
'image/jpg': {
|
|
243
243
|
schema: {
|
|
@@ -310,7 +310,7 @@ it('support multipart/form-data', () => {
|
|
|
310
310
|
},
|
|
311
311
|
},
|
|
312
312
|
responses: {
|
|
313
|
-
|
|
313
|
+
200: {
|
|
314
314
|
content: {
|
|
315
315
|
'image/*': {
|
|
316
316
|
schema: {
|
|
@@ -415,7 +415,7 @@ it('work with example', () => {
|
|
|
415
415
|
},
|
|
416
416
|
},
|
|
417
417
|
responses: {
|
|
418
|
-
|
|
418
|
+
200: {
|
|
419
419
|
content: {
|
|
420
420
|
'application/json': {
|
|
421
421
|
schema: {
|
|
@@ -463,3 +463,181 @@ it('work with example', () => {
|
|
|
463
463
|
},
|
|
464
464
|
})
|
|
465
465
|
})
|
|
466
|
+
|
|
467
|
+
it('should remove params on body', () => {
|
|
468
|
+
const router = oc.router({
|
|
469
|
+
upload: oc.route({ method: 'POST', path: '/upload/{id}' }).input(
|
|
470
|
+
oz.openapi(
|
|
471
|
+
z.object({
|
|
472
|
+
id: z.number(),
|
|
473
|
+
file: z.string().url(),
|
|
474
|
+
}),
|
|
475
|
+
{
|
|
476
|
+
examples: [
|
|
477
|
+
{
|
|
478
|
+
id: 123,
|
|
479
|
+
file: 'https://example.com/file.png',
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
},
|
|
483
|
+
),
|
|
484
|
+
),
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
const spec = generateOpenAPI({
|
|
488
|
+
router,
|
|
489
|
+
info: {
|
|
490
|
+
title: 'test',
|
|
491
|
+
version: '1.0.0',
|
|
492
|
+
},
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
expect(spec).toEqual({
|
|
496
|
+
info: { title: 'test', version: '1.0.0' },
|
|
497
|
+
openapi: '3.1.0',
|
|
498
|
+
paths: {
|
|
499
|
+
'/upload/{id}': {
|
|
500
|
+
post: {
|
|
501
|
+
summary: undefined,
|
|
502
|
+
description: undefined,
|
|
503
|
+
deprecated: undefined,
|
|
504
|
+
tags: undefined,
|
|
505
|
+
operationId: 'upload',
|
|
506
|
+
parameters: [
|
|
507
|
+
{
|
|
508
|
+
name: 'id',
|
|
509
|
+
in: 'path',
|
|
510
|
+
required: true,
|
|
511
|
+
schema: { examples: [123], type: 'number' },
|
|
512
|
+
example: undefined,
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
requestBody: {
|
|
516
|
+
required: false,
|
|
517
|
+
content: {
|
|
518
|
+
'application/json': {
|
|
519
|
+
schema: {
|
|
520
|
+
type: 'object',
|
|
521
|
+
properties: { file: { type: 'string', format: 'uri' } },
|
|
522
|
+
required: ['file'],
|
|
523
|
+
examples: [{ file: 'https://example.com/file.png' }],
|
|
524
|
+
},
|
|
525
|
+
example: undefined,
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
responses: {
|
|
530
|
+
200: {
|
|
531
|
+
description: 'OK',
|
|
532
|
+
content: {
|
|
533
|
+
'application/json': { schema: {}, example: undefined },
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
})
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
it('should remove params on query', () => {
|
|
544
|
+
const router = oc.router({
|
|
545
|
+
upload: oc.route({ method: 'GET', path: '/upload/{id}' }).input(
|
|
546
|
+
oz.openapi(
|
|
547
|
+
z.object({
|
|
548
|
+
id: z.number(),
|
|
549
|
+
file: z.string().url(),
|
|
550
|
+
object: z
|
|
551
|
+
.object({
|
|
552
|
+
name: z.string(),
|
|
553
|
+
})
|
|
554
|
+
.optional(),
|
|
555
|
+
}),
|
|
556
|
+
{
|
|
557
|
+
examples: [
|
|
558
|
+
{
|
|
559
|
+
id: 123,
|
|
560
|
+
file: 'https://example.com/file.png',
|
|
561
|
+
object: { name: 'test' },
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
id: 456,
|
|
565
|
+
file: 'https://example.com/file2.png',
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
},
|
|
569
|
+
),
|
|
570
|
+
),
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
const spec = generateOpenAPI({
|
|
574
|
+
router,
|
|
575
|
+
info: {
|
|
576
|
+
title: 'test',
|
|
577
|
+
version: '1.0.0',
|
|
578
|
+
},
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
expect(spec).toEqual({
|
|
582
|
+
info: { title: 'test', version: '1.0.0' },
|
|
583
|
+
openapi: '3.1.0',
|
|
584
|
+
paths: {
|
|
585
|
+
'/upload/{id}': {
|
|
586
|
+
get: {
|
|
587
|
+
summary: undefined,
|
|
588
|
+
description: undefined,
|
|
589
|
+
deprecated: undefined,
|
|
590
|
+
tags: undefined,
|
|
591
|
+
operationId: 'upload',
|
|
592
|
+
parameters: [
|
|
593
|
+
{
|
|
594
|
+
name: 'id',
|
|
595
|
+
in: 'path',
|
|
596
|
+
required: true,
|
|
597
|
+
schema: { examples: [123, 456], type: 'number' },
|
|
598
|
+
example: undefined,
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
name: 'file',
|
|
602
|
+
in: 'query',
|
|
603
|
+
style: 'deepObject',
|
|
604
|
+
required: true,
|
|
605
|
+
schema: {
|
|
606
|
+
examples: [
|
|
607
|
+
'https://example.com/file.png',
|
|
608
|
+
'https://example.com/file2.png',
|
|
609
|
+
],
|
|
610
|
+
type: 'string',
|
|
611
|
+
format: 'uri',
|
|
612
|
+
},
|
|
613
|
+
example: undefined,
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
name: 'object',
|
|
617
|
+
in: 'query',
|
|
618
|
+
style: 'deepObject',
|
|
619
|
+
required: false,
|
|
620
|
+
schema: {
|
|
621
|
+
examples: [{ name: 'test' }],
|
|
622
|
+
anyOf: undefined,
|
|
623
|
+
type: 'object',
|
|
624
|
+
properties: { name: { type: 'string' } },
|
|
625
|
+
required: ['name'],
|
|
626
|
+
},
|
|
627
|
+
example: undefined,
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
requestBody: undefined,
|
|
631
|
+
responses: {
|
|
632
|
+
200: {
|
|
633
|
+
description: 'OK',
|
|
634
|
+
content: {
|
|
635
|
+
'application/json': { schema: {}, example: undefined },
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
})
|
|
643
|
+
})
|
package/src/generator.ts
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
+
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
1
2
|
import { type ContractRouter, eachContractRouterLeaf } from '@orpc/contract'
|
|
2
3
|
import { type Router, toContractRouter } from '@orpc/server'
|
|
3
|
-
import { findDeepMatches, omit } from '@orpc/shared'
|
|
4
|
+
import { findDeepMatches, isPlainObject, omit } from '@orpc/shared'
|
|
4
5
|
import { preSerialize } from '@orpc/transformer'
|
|
5
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
6
6
|
import {
|
|
7
7
|
type MediaTypeObject,
|
|
8
|
-
type OpenAPIObject,
|
|
9
8
|
OpenApiBuilder,
|
|
9
|
+
type OpenAPIObject,
|
|
10
10
|
type OperationObject,
|
|
11
11
|
type ParameterObject,
|
|
12
12
|
type RequestBodyObject,
|
|
13
13
|
type ResponseObject,
|
|
14
14
|
} from 'openapi3-ts/oas31'
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
extractJSONSchema,
|
|
17
|
+
UNSUPPORTED_JSON_SCHEMA,
|
|
18
|
+
zodToJsonSchema,
|
|
19
|
+
} from './zod-to-json-schema'
|
|
16
20
|
|
|
17
21
|
// Reference: https://spec.openapis.org/oas/v3.1.0.html#style-values
|
|
18
22
|
|
|
@@ -41,17 +45,17 @@ export function generateOpenAPI(
|
|
|
41
45
|
} & Omit<OpenAPIObject, 'openapi'>,
|
|
42
46
|
options?: GenerateOpenAPIOptions,
|
|
43
47
|
): OpenAPIObject {
|
|
44
|
-
const throwOnMissingTagDefinition
|
|
45
|
-
options?.throwOnMissingTagDefinition ?? false
|
|
46
|
-
const ignoreUndefinedPathProcedures
|
|
47
|
-
options?.ignoreUndefinedPathProcedures ?? false
|
|
48
|
+
const throwOnMissingTagDefinition
|
|
49
|
+
= options?.throwOnMissingTagDefinition ?? false
|
|
50
|
+
const ignoreUndefinedPathProcedures
|
|
51
|
+
= options?.ignoreUndefinedPathProcedures ?? false
|
|
48
52
|
|
|
49
53
|
const builder = new OpenApiBuilder({
|
|
50
54
|
...omit(opts, ['router']),
|
|
51
55
|
openapi: '3.1.0',
|
|
52
56
|
})
|
|
53
57
|
|
|
54
|
-
const rootTags = opts.tags?.map(
|
|
58
|
+
const rootTags = opts.tags?.map(tag => tag.name) ?? []
|
|
55
59
|
const router = toContractRouter(opts.router)
|
|
56
60
|
|
|
57
61
|
eachContractRouterLeaf(router, (procedure, path_) => {
|
|
@@ -64,7 +68,7 @@ export function generateOpenAPI(
|
|
|
64
68
|
const path = internal.path ?? `/${path_.map(encodeURIComponent).join('/')}`
|
|
65
69
|
const method = internal.method ?? 'POST'
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
let inputSchema = internal.InputSchema
|
|
68
72
|
? zodToJsonSchema(internal.InputSchema, { mode: 'input' })
|
|
69
73
|
: {}
|
|
70
74
|
const outputSchema = internal.OutputSchema
|
|
@@ -72,7 +76,7 @@ export function generateOpenAPI(
|
|
|
72
76
|
: {}
|
|
73
77
|
|
|
74
78
|
const params: ParameterObject[] | undefined = (() => {
|
|
75
|
-
const names = path.match(
|
|
79
|
+
const names = path.match(/\{([^}]+)\}/g)
|
|
76
80
|
|
|
77
81
|
if (!names || !names.length) {
|
|
78
82
|
return undefined
|
|
@@ -85,12 +89,12 @@ export function generateOpenAPI(
|
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
return names
|
|
88
|
-
.map(
|
|
92
|
+
.map(raw => raw.slice(1, -1))
|
|
89
93
|
.map((name) => {
|
|
90
|
-
|
|
94
|
+
let schema = inputSchema.properties?.[name]
|
|
91
95
|
const required = inputSchema.required?.includes(name)
|
|
92
96
|
|
|
93
|
-
if (
|
|
97
|
+
if (schema === undefined) {
|
|
94
98
|
throw new Error(
|
|
95
99
|
`Parameter ${name} is missing in input schema [${path_.join('.')}]`,
|
|
96
100
|
)
|
|
@@ -102,6 +106,55 @@ export function generateOpenAPI(
|
|
|
102
106
|
)
|
|
103
107
|
}
|
|
104
108
|
|
|
109
|
+
const examples = inputSchema.examples
|
|
110
|
+
?.filter((example) => {
|
|
111
|
+
return isPlainObject(example) && name in example
|
|
112
|
+
})
|
|
113
|
+
.map((example) => {
|
|
114
|
+
return example[name]
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
schema = {
|
|
118
|
+
examples: examples?.length ? examples : undefined,
|
|
119
|
+
...(schema === true
|
|
120
|
+
? {}
|
|
121
|
+
: schema === false
|
|
122
|
+
? UNSUPPORTED_JSON_SCHEMA
|
|
123
|
+
: schema),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
inputSchema = {
|
|
127
|
+
...inputSchema,
|
|
128
|
+
properties: inputSchema.properties
|
|
129
|
+
? Object.entries(inputSchema.properties).reduce(
|
|
130
|
+
(acc, [key, value]) => {
|
|
131
|
+
if (key !== name) {
|
|
132
|
+
acc[key] = value
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return acc
|
|
136
|
+
},
|
|
137
|
+
{} as Record<string, JSONSchema>,
|
|
138
|
+
)
|
|
139
|
+
: undefined,
|
|
140
|
+
required: inputSchema.required?.filter(v => v !== name),
|
|
141
|
+
examples: inputSchema.examples?.map((example) => {
|
|
142
|
+
if (!isPlainObject(example))
|
|
143
|
+
return example
|
|
144
|
+
|
|
145
|
+
return Object.entries(example).reduce(
|
|
146
|
+
(acc, [key, value]) => {
|
|
147
|
+
if (key !== name) {
|
|
148
|
+
acc[key] = value
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return acc
|
|
152
|
+
},
|
|
153
|
+
{} as Record<string, unknown>,
|
|
154
|
+
)
|
|
155
|
+
}),
|
|
156
|
+
}
|
|
157
|
+
|
|
105
158
|
return {
|
|
106
159
|
name,
|
|
107
160
|
in: 'path',
|
|
@@ -123,18 +176,35 @@ export function generateOpenAPI(
|
|
|
123
176
|
)
|
|
124
177
|
}
|
|
125
178
|
|
|
126
|
-
return Object.entries(inputSchema.properties ?? {})
|
|
127
|
-
|
|
128
|
-
|
|
179
|
+
return Object.entries(inputSchema.properties ?? {}).map(
|
|
180
|
+
([name, schema]) => {
|
|
181
|
+
const examples = inputSchema.examples
|
|
182
|
+
?.filter((example) => {
|
|
183
|
+
return isPlainObject(example) && name in example
|
|
184
|
+
})
|
|
185
|
+
.map((example) => {
|
|
186
|
+
return example[name]
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const schema_ = {
|
|
190
|
+
examples: examples?.length ? examples : undefined,
|
|
191
|
+
...(schema === true
|
|
192
|
+
? {}
|
|
193
|
+
: schema === false
|
|
194
|
+
? UNSUPPORTED_JSON_SCHEMA
|
|
195
|
+
: schema),
|
|
196
|
+
}
|
|
197
|
+
|
|
129
198
|
return {
|
|
130
199
|
name,
|
|
131
200
|
in: 'query',
|
|
132
201
|
style: 'deepObject',
|
|
133
|
-
required:
|
|
134
|
-
schema:
|
|
202
|
+
required: inputSchema?.required?.includes(name) ?? false,
|
|
203
|
+
schema: schema_ as any,
|
|
135
204
|
example: internal.inputExample?.[name],
|
|
136
205
|
}
|
|
137
|
-
}
|
|
206
|
+
},
|
|
207
|
+
)
|
|
138
208
|
})()
|
|
139
209
|
|
|
140
210
|
const parameters = [...(params ?? []), ...(query ?? [])]
|
|
@@ -151,8 +221,8 @@ export function generateOpenAPI(
|
|
|
151
221
|
contentMediaType: string
|
|
152
222
|
})[]
|
|
153
223
|
|
|
154
|
-
const isStillHasFileSchema
|
|
155
|
-
findDeepMatches(isFileSchema, schema).values.length > 0
|
|
224
|
+
const isStillHasFileSchema
|
|
225
|
+
= findDeepMatches(isFileSchema, schema).values.length > 0
|
|
156
226
|
|
|
157
227
|
if (files.length) {
|
|
158
228
|
parameters.push({
|
|
@@ -199,8 +269,8 @@ export function generateOpenAPI(
|
|
|
199
269
|
contentMediaType: string
|
|
200
270
|
})[]
|
|
201
271
|
|
|
202
|
-
const isStillHasFileSchema
|
|
203
|
-
findDeepMatches(isFileSchema, schema).values.length > 0
|
|
272
|
+
const isStillHasFileSchema
|
|
273
|
+
= findDeepMatches(isFileSchema, schema).values.length > 0
|
|
204
274
|
|
|
205
275
|
const content: Record<string, MediaTypeObject> = {}
|
|
206
276
|
|
|
@@ -226,7 +296,7 @@ export function generateOpenAPI(
|
|
|
226
296
|
})()
|
|
227
297
|
|
|
228
298
|
if (throwOnMissingTagDefinition && internal.tags) {
|
|
229
|
-
const missingTag = internal.tags.find(
|
|
299
|
+
const missingTag = internal.tags.find(tag => !rootTags.includes(tag))
|
|
230
300
|
|
|
231
301
|
if (missingTag !== undefined) {
|
|
232
302
|
throw new Error(
|
|
@@ -244,7 +314,7 @@ export function generateOpenAPI(
|
|
|
244
314
|
parameters: parameters.length ? parameters : undefined,
|
|
245
315
|
requestBody,
|
|
246
316
|
responses: {
|
|
247
|
-
|
|
317
|
+
200: successResponse,
|
|
248
318
|
},
|
|
249
319
|
}
|
|
250
320
|
|
|
@@ -257,11 +327,12 @@ export function generateOpenAPI(
|
|
|
257
327
|
}
|
|
258
328
|
|
|
259
329
|
function isFileSchema(schema: unknown) {
|
|
260
|
-
if (typeof schema !== 'object' || schema === null)
|
|
330
|
+
if (typeof schema !== 'object' || schema === null)
|
|
331
|
+
return false
|
|
261
332
|
return (
|
|
262
|
-
'type' in schema
|
|
263
|
-
'contentMediaType' in schema
|
|
264
|
-
typeof schema.type === 'string'
|
|
265
|
-
typeof schema.contentMediaType === 'string'
|
|
333
|
+
'type' in schema
|
|
334
|
+
&& 'contentMediaType' in schema
|
|
335
|
+
&& typeof schema.type === 'string'
|
|
336
|
+
&& typeof schema.contentMediaType === 'string'
|
|
266
337
|
)
|
|
267
338
|
}
|
|
@@ -244,7 +244,7 @@ describe('special types', () => {
|
|
|
244
244
|
|
|
245
245
|
describe('transform and effects', () => {
|
|
246
246
|
it('should handle transform effects based on mode', () => {
|
|
247
|
-
const schema = z.string().transform(
|
|
247
|
+
const schema = z.string().transform(val => val.length)
|
|
248
248
|
|
|
249
249
|
expect(zodToJsonSchema(schema, { mode: 'input' })).toEqual({
|
|
250
250
|
type: 'string',
|
|
@@ -14,11 +14,8 @@ import {
|
|
|
14
14
|
type KeySchema,
|
|
15
15
|
type ZodAny,
|
|
16
16
|
type ZodArray,
|
|
17
|
-
type ZodBigInt,
|
|
18
|
-
type ZodBoolean,
|
|
19
17
|
type ZodBranded,
|
|
20
18
|
type ZodCatch,
|
|
21
|
-
type ZodDate,
|
|
22
19
|
type ZodDefault,
|
|
23
20
|
type ZodDiscriminatedUnion,
|
|
24
21
|
type ZodEffects,
|
|
@@ -28,7 +25,6 @@ import {
|
|
|
28
25
|
type ZodLazy,
|
|
29
26
|
type ZodLiteral,
|
|
30
27
|
type ZodMap,
|
|
31
|
-
type ZodNaN,
|
|
32
28
|
type ZodNativeEnum,
|
|
33
29
|
type ZodNullable,
|
|
34
30
|
type ZodNumber,
|
|
@@ -213,8 +209,8 @@ export function zodToJsonSchema(
|
|
|
213
209
|
json.pattern = `${escapeStringRegexp(check.value)}$`
|
|
214
210
|
break
|
|
215
211
|
case 'emoji':
|
|
216
|
-
json.pattern
|
|
217
|
-
'^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$'
|
|
212
|
+
json.pattern
|
|
213
|
+
= '^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$'
|
|
218
214
|
break
|
|
219
215
|
case 'nanoid':
|
|
220
216
|
json.pattern = '^[a-zA-Z0-9_-]{21}$'
|
|
@@ -278,14 +274,10 @@ export function zodToJsonSchema(
|
|
|
278
274
|
}
|
|
279
275
|
|
|
280
276
|
case ZodFirstPartyTypeKind.ZodNaN: {
|
|
281
|
-
const schema_ = schema as ZodNaN
|
|
282
|
-
|
|
283
277
|
return { const: 'NaN' }
|
|
284
278
|
}
|
|
285
279
|
|
|
286
280
|
case ZodFirstPartyTypeKind.ZodBigInt: {
|
|
287
|
-
const schema_ = schema as ZodBigInt
|
|
288
|
-
|
|
289
281
|
const json: JSONSchema = { type: 'string', pattern: '^-?[0-9]+$' }
|
|
290
282
|
|
|
291
283
|
// WARN: ignore checks
|
|
@@ -294,14 +286,10 @@ export function zodToJsonSchema(
|
|
|
294
286
|
}
|
|
295
287
|
|
|
296
288
|
case ZodFirstPartyTypeKind.ZodBoolean: {
|
|
297
|
-
const schema_ = schema as ZodBoolean
|
|
298
|
-
|
|
299
289
|
return { type: 'boolean' }
|
|
300
290
|
}
|
|
301
291
|
|
|
302
292
|
case ZodFirstPartyTypeKind.ZodDate: {
|
|
303
|
-
const schema_ = schema as ZodDate
|
|
304
|
-
|
|
305
293
|
const jsonSchema: JSONSchema = { type: 'string', format: Format.Date }
|
|
306
294
|
|
|
307
295
|
// WARN: ignore checks
|
|
@@ -398,7 +386,7 @@ export function zodToJsonSchema(
|
|
|
398
386
|
for (const [key, value] of Object.entries(schema_.shape)) {
|
|
399
387
|
const { schema, matches } = extractJSONSchema(
|
|
400
388
|
zodToJsonSchema(value, childOptions),
|
|
401
|
-
|
|
389
|
+
schema => schema === UNDEFINED_JSON_SCHEMA,
|
|
402
390
|
)
|
|
403
391
|
|
|
404
392
|
if (schema) {
|
|
@@ -423,14 +411,15 @@ export function zodToJsonSchema(
|
|
|
423
411
|
childOptions,
|
|
424
412
|
)
|
|
425
413
|
if (schema_._def.unknownKeys === 'strict') {
|
|
426
|
-
json.additionalProperties
|
|
427
|
-
additionalProperties === UNSUPPORTED_JSON_SCHEMA
|
|
414
|
+
json.additionalProperties
|
|
415
|
+
= additionalProperties === UNSUPPORTED_JSON_SCHEMA
|
|
428
416
|
? false
|
|
429
417
|
: additionalProperties
|
|
430
|
-
}
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
431
420
|
if (
|
|
432
|
-
additionalProperties
|
|
433
|
-
additionalProperties !== UNSUPPORTED_JSON_SCHEMA
|
|
421
|
+
additionalProperties
|
|
422
|
+
&& additionalProperties !== UNSUPPORTED_JSON_SCHEMA
|
|
434
423
|
) {
|
|
435
424
|
json.additionalProperties = additionalProperties
|
|
436
425
|
}
|
|
@@ -551,8 +540,8 @@ export function zodToJsonSchema(
|
|
|
551
540
|
const schema_ = schema as ZodEffects<ZodTypeAny>
|
|
552
541
|
|
|
553
542
|
if (
|
|
554
|
-
schema_._def.effect.type === 'transform'
|
|
555
|
-
childOptions?.mode === 'output'
|
|
543
|
+
schema_._def.effect.type === 'transform'
|
|
544
|
+
&& childOptions?.mode === 'output'
|
|
556
545
|
) {
|
|
557
546
|
return {}
|
|
558
547
|
}
|
|
@@ -602,7 +591,7 @@ export function extractJSONSchema(
|
|
|
602
591
|
schema: JSONSchema,
|
|
603
592
|
check: (schema: JSONSchema) => boolean,
|
|
604
593
|
matches: JSONSchema[] = [],
|
|
605
|
-
): { schema: JSONSchema | undefined
|
|
594
|
+
): { schema: JSONSchema | undefined, matches: JSONSchema[] } {
|
|
606
595
|
if (check(schema)) {
|
|
607
596
|
matches.push(schema)
|
|
608
597
|
return { schema: undefined, matches }
|
|
@@ -615,14 +604,14 @@ export function extractJSONSchema(
|
|
|
615
604
|
// TODO: $ref
|
|
616
605
|
|
|
617
606
|
if (
|
|
618
|
-
schema.anyOf
|
|
619
|
-
Object.keys(schema).every(
|
|
620
|
-
|
|
607
|
+
schema.anyOf
|
|
608
|
+
&& Object.keys(schema).every(
|
|
609
|
+
k => k === 'anyOf' || NON_LOGIC_KEYWORDS.includes(k as any),
|
|
621
610
|
)
|
|
622
611
|
) {
|
|
623
612
|
const anyOf = schema.anyOf
|
|
624
|
-
.map(
|
|
625
|
-
.filter(
|
|
613
|
+
.map(s => extractJSONSchema(s, check, matches).schema)
|
|
614
|
+
.filter(v => !!v)
|
|
626
615
|
|
|
627
616
|
if (anyOf.length === 1 && typeof anyOf[0] === 'object') {
|
|
628
617
|
return { schema: { ...schema, anyOf: undefined, ...anyOf[0] }, matches }
|
|
@@ -640,14 +629,14 @@ export function extractJSONSchema(
|
|
|
640
629
|
// TODO: $ref
|
|
641
630
|
|
|
642
631
|
if (
|
|
643
|
-
schema.oneOf
|
|
644
|
-
Object.keys(schema).every(
|
|
645
|
-
|
|
632
|
+
schema.oneOf
|
|
633
|
+
&& Object.keys(schema).every(
|
|
634
|
+
k => k === 'oneOf' || NON_LOGIC_KEYWORDS.includes(k as any),
|
|
646
635
|
)
|
|
647
636
|
) {
|
|
648
637
|
const oneOf = schema.oneOf
|
|
649
|
-
.map(
|
|
650
|
-
.filter(
|
|
638
|
+
.map(s => extractJSONSchema(s, check, matches).schema)
|
|
639
|
+
.filter(v => !!v)
|
|
651
640
|
|
|
652
641
|
if (oneOf.length === 1 && typeof oneOf[0] === 'object') {
|
|
653
642
|
return { schema: { ...schema, oneOf: undefined, ...oneOf[0] }, matches }
|