Skip to content

Commit 01cd1d0

Browse files
fix(zui): keep descriptions when mapping zod to json-schema (#593)
1 parent 8615bdd commit 01cd1d0

File tree

10 files changed

+80
-22
lines changed

10 files changed

+80
-22
lines changed

zui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bpinternal/zui",
3-
"version": "0.21.0",
3+
"version": "0.21.1",
44
"description": "A fork of Zod with additional features",
55
"type": "module",
66
"source": "./src/index.ts",

zui/src/transforms/common/json-schema.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type ZuiExtension<Def extends Partial<z.ZodDef> = {}> = { def?: Def } & ZuiExten
2828
type JsonData = string | number | boolean | null | JsonData[] | { [key: string]: JsonData }
2929
type BaseZuiJsonSchema<Def extends Partial<z.ZodDef> = {}> = util.Satisfies<
3030
{
31+
description?: string
3132
readOnly?: boolean
3233
default?: JsonData
3334
['x-zui']?: ZuiExtension<Def>
@@ -82,8 +83,8 @@ type _ObjectSchema = util.Satisfies<
8283
{
8384
type: 'object'
8485
properties: { [key: string]: ZuiJsonSchema }
85-
additionalProperties?: ZuiJsonSchema
86-
required: string[]
86+
additionalProperties?: ZuiJsonSchema | boolean
87+
required?: string[]
8788
},
8889
JSONSchema7
8990
>

zui/src/transforms/zui-to-json-schema-next/index.test.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ describe('zuiToJsonSchemaNext', () => {
101101
type: 'object',
102102
properties: { name: { type: 'string' } },
103103
required: ['name'],
104-
additionalProperties: { not: true },
104+
additionalProperties: false,
105105
})
106106
})
107107

@@ -111,7 +111,7 @@ describe('zuiToJsonSchemaNext', () => {
111111
type: 'object',
112112
properties: { name: { type: 'string' } },
113113
required: ['name'],
114-
additionalProperties: {},
114+
additionalProperties: true,
115115
})
116116
})
117117

@@ -125,6 +125,25 @@ describe('zuiToJsonSchemaNext', () => {
125125
})
126126
})
127127

128+
test('should preserve ZodObject nested properties descriptions', () => {
129+
const description = 'The ID or Name of the table (e.g. tblFnqcm4zLVKn85A or articles)'
130+
const schema = toJsonSchema(
131+
z.object({
132+
tableIdOrName: z.string().describe(description),
133+
}),
134+
)
135+
expect(schema).toEqual({
136+
type: 'object',
137+
properties: {
138+
tableIdOrName: {
139+
type: 'string',
140+
description,
141+
},
142+
},
143+
required: ['tableIdOrName'],
144+
})
145+
})
146+
128147
test('should map ZodUnion to UnionSchema', () => {
129148
const schema = toJsonSchema(z.union([z.string(), z.number()]))
130149
expect(schema).toEqual({

zui/src/transforms/zui-to-json-schema-next/index.ts

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { ZuiExtensionObject } from '../../ui/types'
21
import z from '../../z'
32
import * as json from '../common/json-schema'
43
import * as err from '../common/errors'
@@ -35,30 +34,39 @@ export function toJsonSchema(schema: z.Schema): json.ZuiJsonSchema {
3534
})
3635

3736
case z.ZodFirstPartyTypeKind.ZodBoolean:
38-
return { type: 'boolean', 'x-zui': def['x-zui'] } satisfies json.BooleanSchema
37+
return {
38+
type: 'boolean',
39+
description: def.description,
40+
'x-zui': def['x-zui'],
41+
} satisfies json.BooleanSchema
3942

4043
case z.ZodFirstPartyTypeKind.ZodDate:
4144
throw new err.UnsupportedZuiToJsonSchemaError(z.ZodFirstPartyTypeKind.ZodDate, {
4245
suggestedAlternative: 'use z.string().datetime() instead',
4346
})
4447

4548
case z.ZodFirstPartyTypeKind.ZodUndefined:
46-
return undefinedSchema(def['x-zui'])
49+
return undefinedSchema(def)
4750

4851
case z.ZodFirstPartyTypeKind.ZodNull:
49-
return nullSchema(def['x-zui'])
52+
return nullSchema(def)
5053

5154
case z.ZodFirstPartyTypeKind.ZodAny:
52-
return { 'x-zui': def['x-zui'] } satisfies json.AnySchema
55+
return {
56+
description: def.description,
57+
'x-zui': def['x-zui'],
58+
} satisfies json.AnySchema
5359

5460
case z.ZodFirstPartyTypeKind.ZodUnknown:
5561
return {
62+
description: def.description,
5663
'x-zui': { ...def['x-zui'], def: { typeName: z.ZodFirstPartyTypeKind.ZodUnknown } },
5764
}
5865

5966
case z.ZodFirstPartyTypeKind.ZodNever:
6067
return {
6168
not: true,
69+
description: def.description,
6270
'x-zui': def['x-zui'],
6371
} satisfies json.NeverSchema
6472

@@ -70,30 +78,40 @@ export function toJsonSchema(schema: z.Schema): json.ZuiJsonSchema {
7078

7179
case z.ZodFirstPartyTypeKind.ZodObject:
7280
const shape = Object.entries(def.shape())
73-
const required = shape.filter(([_, value]) => !value.isOptional()).map(([key]) => key)
81+
const requiredProperties = shape.filter(([_, value]) => !value.isOptional())
82+
const required = requiredProperties.length ? requiredProperties.map(([key]) => key) : undefined
7483
const properties = shape
7584
.map(([key, value]) => [key, _toRequired(value)] satisfies [string, z.ZodType])
7685
.map(([key, value]) => [key, toJsonSchema(value)] satisfies [string, json.ZuiJsonSchema])
7786

78-
const zAdditionalProperties = (schema as z.ZodObject).additionalProperties()
79-
const additionalProperties = zAdditionalProperties ? toJsonSchema(zAdditionalProperties) : undefined
87+
let additionalProperties: json.ObjectSchema['additionalProperties'] = undefined
88+
if (def.unknownKeys instanceof z.ZodType) {
89+
additionalProperties = toJsonSchema(def.unknownKeys)
90+
} else if (def.unknownKeys === 'passthrough') {
91+
additionalProperties = true
92+
} else if (def.unknownKeys === 'strict') {
93+
additionalProperties = false
94+
}
8095

8196
return {
8297
type: 'object',
98+
description: def.description,
8399
properties: Object.fromEntries(properties),
84100
required,
85-
'x-zui': def['x-zui'],
86101
additionalProperties,
102+
'x-zui': def['x-zui'],
87103
} satisfies json.ObjectSchema
88104

89105
case z.ZodFirstPartyTypeKind.ZodUnion:
90106
return {
107+
description: def.description,
91108
anyOf: def.options.map((option) => toJsonSchema(option)),
92109
'x-zui': def['x-zui'],
93110
} satisfies json.UnionSchema
94111

95112
case z.ZodFirstPartyTypeKind.ZodDiscriminatedUnion:
96113
return {
114+
description: def.description,
97115
anyOf: def.options.map((option) => toJsonSchema(option)),
98116
'x-zui': {
99117
...def['x-zui'],
@@ -103,6 +121,7 @@ export function toJsonSchema(schema: z.Schema): json.ZuiJsonSchema {
103121

104122
case z.ZodFirstPartyTypeKind.ZodIntersection:
105123
return {
124+
description: def.description,
106125
allOf: [toJsonSchema(def.left), toJsonSchema(def.right)],
107126
'x-zui': def['x-zui'],
108127
} satisfies json.IntersectionSchema
@@ -113,6 +132,7 @@ export function toJsonSchema(schema: z.Schema): json.ZuiJsonSchema {
113132
case z.ZodFirstPartyTypeKind.ZodRecord:
114133
return {
115134
type: 'object',
135+
description: def.description,
116136
additionalProperties: toJsonSchema(def.valueType),
117137
'x-zui': def['x-zui'],
118138
} satisfies json.RecordSchema
@@ -133,25 +153,28 @@ export function toJsonSchema(schema: z.Schema): json.ZuiJsonSchema {
133153
if (typeof def.value === 'string') {
134154
return {
135155
type: 'string',
156+
description: def.description,
136157
const: def.value,
137158
'x-zui': def['x-zui'],
138159
} satisfies json.LiteralStringSchema
139160
} else if (typeof def.value === 'number') {
140161
return {
141162
type: 'number',
163+
description: def.description,
142164
const: def.value,
143165
'x-zui': def['x-zui'],
144166
} satisfies json.LiteralNumberSchema
145167
} else if (typeof def.value === 'boolean') {
146168
return {
147169
type: 'boolean',
170+
description: def.description,
148171
const: def.value,
149172
'x-zui': def['x-zui'],
150173
} satisfies json.LiteralBooleanSchema
151174
} else if (def.value === null) {
152-
return nullSchema(def['x-zui'])
175+
return nullSchema(def)
153176
} else if (def.value === undefined) {
154-
return undefinedSchema(def['x-zui'])
177+
return undefinedSchema(def)
155178
} else {
156179
z.util.assertEqual<bigint | symbol, typeof def.value>(true)
157180
const unsupportedLiteral = typeof def.value
@@ -161,6 +184,7 @@ export function toJsonSchema(schema: z.Schema): json.ZuiJsonSchema {
161184
case z.ZodFirstPartyTypeKind.ZodEnum:
162185
return {
163186
type: 'string',
187+
description: def.description,
164188
enum: def.values,
165189
'x-zui': def['x-zui'],
166190
} satisfies json.EnumSchema
@@ -173,6 +197,7 @@ export function toJsonSchema(schema: z.Schema): json.ZuiJsonSchema {
173197

174198
case z.ZodFirstPartyTypeKind.ZodOptional:
175199
return {
200+
description: def.description,
176201
anyOf: [toJsonSchema(def.innerType), undefinedSchema()],
177202
'x-zui': {
178203
...def['x-zui'],
@@ -192,8 +217,8 @@ export function toJsonSchema(schema: z.Schema): json.ZuiJsonSchema {
192217
case z.ZodFirstPartyTypeKind.ZodDefault:
193218
// ZodDefault is not treated as a metadata root so we don't need to preserve x-zui
194219
return {
195-
default: def.defaultValue(),
196220
...toJsonSchema(def.innerType),
221+
default: def.defaultValue(),
197222
}
198223

199224
case z.ZodFirstPartyTypeKind.ZodCatch:
@@ -215,13 +240,14 @@ export function toJsonSchema(schema: z.Schema): json.ZuiJsonSchema {
215240
case z.ZodFirstPartyTypeKind.ZodReadonly:
216241
// ZodReadonly is not treated as a metadata root so we don't need to preserve x-zui
217242
return {
218-
readOnly: true,
219243
...toJsonSchema(def.innerType),
244+
readOnly: true,
220245
}
221246

222247
case z.ZodFirstPartyTypeKind.ZodRef:
223248
return {
224249
$ref: def.uri,
250+
description: def.description,
225251
'x-zui': def['x-zui'],
226252
}
227253

@@ -259,12 +285,14 @@ const _toRequired = (schema: z.ZodType): z.ZodType => {
259285
return newSchema
260286
}
261287

262-
const undefinedSchema = (xZui?: ZuiExtensionObject): json.UndefinedSchema => ({
288+
const undefinedSchema = (def?: z.ZodTypeDef): json.UndefinedSchema => ({
263289
not: true,
264-
'x-zui': { ...xZui, def: { typeName: z.ZodFirstPartyTypeKind.ZodUndefined } },
290+
description: def?.description,
291+
'x-zui': { ...def?.['x-zui'], def: { typeName: z.ZodFirstPartyTypeKind.ZodUndefined } },
265292
})
266293

267-
const nullSchema = (xZui?: ZuiExtensionObject): json.NullSchema => ({
294+
const nullSchema = (def?: z.ZodTypeDef): json.NullSchema => ({
268295
type: 'null',
269-
'x-zui': xZui,
296+
description: def?.description,
297+
'x-zui': def?.['x-zui'],
270298
})

zui/src/transforms/zui-to-json-schema-next/type-processors/array.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ export const zodArrayToJsonArray = (
88
): json.ArraySchema => {
99
const schema: json.ArraySchema = {
1010
type: 'array',
11+
description: zodArray.description,
1112
items: toSchema(zodArray._def.type),
13+
'x-zui': zodArray._def['x-zui'],
1214
}
1315

1416
if (zodArray._def[zuiKey]) {

zui/src/transforms/zui-to-json-schema-next/type-processors/number.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import * as json from '../../common/json-schema'
55
export const zodNumberToJsonNumber = (zodNumber: z.ZodNumber): json.NumberSchema => {
66
const schema: json.NumberSchema = {
77
type: 'number',
8+
description: zodNumber.description,
9+
'x-zui': zodNumber._def['x-zui'],
810
}
911

1012
if (zodNumber._def[zuiKey]) {

zui/src/transforms/zui-to-json-schema-next/type-processors/set.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ export const zodSetToJsonSet = (
88
): json.SetSchema => {
99
const schema: json.SetSchema = {
1010
type: 'array',
11+
description: zodSet.description,
1112
uniqueItems: true,
1213
items: toSchema(zodSet._def.valueType),
14+
'x-zui': zodSet._def['x-zui'],
1315
}
1416

1517
if (zodSet._def[zuiKey]) {

zui/src/transforms/zui-to-json-schema-next/type-processors/string.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { zodPatterns } from '../../zui-to-json-schema/parsers/string'
88
export const zodStringToJsonString = (zodString: z.ZodString): json.StringSchema => {
99
const schema: json.StringSchema = {
1010
type: 'string',
11+
description: zodString.description,
12+
'x-zui': zodString._def['x-zui'],
1113
}
1214

1315
if (zodString._def[zuiKey]) {

zui/src/transforms/zui-to-json-schema-next/type-processors/tuple.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const zodTupleToJsonTuple = (
88
): json.TupleSchema => {
99
const schema: json.TupleSchema = {
1010
type: 'array',
11+
description: zodTuple.description,
1112
items: zodTuple._def.items.map((item) => toSchema(item)),
1213
}
1314

zui/src/z/types/basetype/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ export abstract class ZodType<Output = any, Def extends ZodTypeDef = ZodTypeDef,
458458
}
459459

460460
describe(description: string): this {
461+
// should set the description on the _metadataRoot
461462
const This = (this as any).constructor
462463
return new This({
463464
...this._def,

0 commit comments

Comments
 (0)