Skip to content

Commit 3829416

Browse files
authored
Schema compliance adjustment (#2582)
Validating the the result is OpenAPI compliant. Addressing todo on ensuring the depiction complies to OpenAPI SchemaObejct
1 parent ab4ec0d commit 3829416

3 files changed

Lines changed: 78 additions & 55 deletions

File tree

example/example.documentation.yaml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ paths:
1616
required: true
1717
description: a numeric string containing the id of the user
1818
schema:
19-
description: a numeric string containing the id of the user
2019
type: string
20+
description: a numeric string containing the id of the user
2121
format: regex
2222
pattern: \d+
2323
responses:
@@ -85,8 +85,8 @@ paths:
8585
required: true
8686
description: numeric string
8787
schema:
88-
description: numeric string
8988
type: string
89+
description: numeric string
9090
format: regex
9191
pattern: \d+
9292
responses:
@@ -107,10 +107,10 @@ paths:
107107
required: true
108108
description: PATCH /v1/user/:id Parameter
109109
schema:
110-
examples:
111-
- "1234567890"
112110
type: string
113111
minLength: 1
112+
examples:
113+
- "1234567890"
114114
examples:
115115
example1:
116116
value: "1234567890"
@@ -119,9 +119,9 @@ paths:
119119
required: true
120120
description: PATCH /v1/user/:id Parameter
121121
schema:
122+
type: string
122123
examples:
123124
- "12"
124-
type: string
125125
examples:
126126
example1:
127127
value: "12"
@@ -133,15 +133,15 @@ paths:
133133
type: object
134134
properties:
135135
key:
136+
type: string
137+
minLength: 1
136138
examples:
137139
- 1234-5678-90
140+
name:
138141
type: string
139142
minLength: 1
140-
name:
141143
examples:
142144
- John Doe
143-
type: string
144-
minLength: 1
145145
birthday:
146146
description: YYYY-MM-DDTHH:mm:ss.sssZ
147147
type: string
@@ -179,9 +179,9 @@ paths:
179179
type: object
180180
properties:
181181
name:
182+
type: string
182183
examples:
183184
- John Doe
184-
type: string
185185
createdAt:
186186
description: YYYY-MM-DDTHH:mm:ss.sssZ
187187
type: string
@@ -578,9 +578,9 @@ paths:
578578
required: false
579579
description: for testing error response
580580
schema:
581+
type: string
581582
deprecated: true
582583
description: for testing error response
583-
type: string
584584
responses:
585585
"200":
586586
description: GET /v1/events/stream Positive response

express-zod-api/src/documentation-helpers.ts

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -193,18 +193,42 @@ export const onIntersection: Overrider = ({ jsonSchema }) => {
193193
export const onNullable: Overrider = ({ jsonSchema }) => {
194194
if (!jsonSchema.anyOf) return;
195195
const original = jsonSchema.anyOf[0];
196-
Object.assign(original, { type: makeNullableType(original) });
196+
Object.assign(original, { type: makeNullableType(original.type) });
197197
Object.assign(jsonSchema, original);
198198
delete jsonSchema.anyOf;
199199
};
200200

201201
const isSupportedType = (subject: string): subject is SchemaObjectType =>
202202
subject in samples;
203203

204+
const ensureCompliance = ({
205+
$ref,
206+
type,
207+
allOf,
208+
oneOf,
209+
anyOf,
210+
not,
211+
...rest
212+
}: JSONSchema.BaseSchema): SchemaObject | ReferenceObject => {
213+
if ($ref) return { $ref };
214+
const valid: SchemaObject = {
215+
type: Array.isArray(type)
216+
? type.filter(isSupportedType)
217+
: type && isSupportedType(type)
218+
? type
219+
: undefined,
220+
...rest,
221+
};
222+
if (allOf) valid.allOf = allOf.map(ensureCompliance);
223+
if (oneOf) valid.oneOf = oneOf.map(ensureCompliance);
224+
if (anyOf) valid.anyOf = anyOf.map(ensureCompliance);
225+
if (not) valid.not = ensureCompliance(not);
226+
return valid;
227+
};
228+
204229
export const onDateIn: Overrider = ({ jsonSchema }, ctx) => {
205230
if (ctx.isResponse)
206231
throw new DocumentationError("Please use ez.dateOut() for output.", ctx);
207-
unref(jsonSchema);
208232
delete jsonSchema.anyOf; // undo default
209233
Object.assign(jsonSchema, {
210234
description: "YYYY-MM-DDTHH:mm:ss.sssZ",
@@ -254,15 +278,18 @@ const makeSample = (depicted: SchemaObject) => {
254278
return samples?.[firstType];
255279
};
256280

257-
const makeNullableType = ({
258-
type,
259-
}: JSONSchema.BaseSchema | SchemaObject):
260-
| SchemaObjectType
261-
| SchemaObjectType[] => {
262-
if (type === "null") return type;
263-
if (typeof type === "string")
264-
return isSupportedType(type) ? [type, "null"] : "null";
265-
return type ? [...new Set(type).add("null")] : "null";
281+
/** @since v24.0.0 does not return null for undefined */
282+
const makeNullableType = (
283+
current:
284+
| JSONSchema.BaseSchema["type"]
285+
| Array<NonNullable<JSONSchema.BaseSchema["type"]>>,
286+
): typeof current => {
287+
if (current === ("null" satisfies SchemaObjectType)) return current;
288+
if (typeof current === "string")
289+
return [current, "null" satisfies SchemaObjectType];
290+
return (
291+
current && [...new Set(current).add("null" satisfies SchemaObjectType)]
292+
);
266293
};
267294

268295
export const onPipeline: Overrider = ({ zodSchema, jsonSchema }, ctx) => {
@@ -296,7 +323,6 @@ export const onPipeline: Overrider = ({ zodSchema, jsonSchema }, ctx) => {
296323
};
297324

298325
export const onRaw: Overrider = ({ jsonSchema }) => {
299-
unref(jsonSchema);
300326
if (jsonSchema.type !== "object") return;
301327
const objSchema = jsonSchema as JSONSchema.ObjectSchema;
302328
if (!objSchema.properties) return;
@@ -437,12 +463,8 @@ const overrides: Partial<Record<FirstPartyKind | ProprietaryBrand, Overrider>> =
437463
};
438464

439465
const onEach: Overrider = ({ zodSchema, jsonSchema }, { isResponse }) => {
440-
if (
441-
!isResponse &&
442-
jsonSchema.type !== undefined &&
443-
doesAccept(zodSchema, null)
444-
)
445-
Object.assign(jsonSchema, { type: makeNullableType(jsonSchema) });
466+
if (!isResponse && doesAccept(zodSchema, null))
467+
Object.assign(jsonSchema, { type: makeNullableType(jsonSchema.type) });
446468
const examples = getExamples({
447469
schema: zodSchema,
448470
variant: isResponse ? "parsed" : "original",
@@ -468,14 +490,14 @@ const fixReferences = (
468490
const actualName = entry.$ref.split("/").pop()!;
469491
const depiction = defs[actualName];
470492
if (depiction)
471-
entry.$ref = ctx.makeRef(depiction, depiction as SchemaObject).$ref; // @todo see below
493+
entry.$ref = ctx.makeRef(depiction, ensureCompliance(depiction)).$ref;
472494
continue;
473495
}
474496
stack.push(...R.values(entry));
475497
}
476498
if (R.is(Array, entry)) stack.push(...R.values(entry));
477499
}
478-
return subject as SchemaObject; // @todo ideally, there should be a method to ensure that
500+
return ensureCompliance(subject);
479501
};
480502

481503
/** @link https://github.com/colinhacks/zod/issues/4275 */
@@ -500,6 +522,7 @@ const depict = (
500522
unrepresentable: "any",
501523
io: ctx.isResponse ? "output" : "input",
502524
override: (zodCtx) => {
525+
unref(zodCtx.jsonSchema);
503526
const { brand } =
504527
globalRegistry.get(zodCtx.zodSchema)?.[metaSymbol] ?? {};
505528
rules[

0 commit comments

Comments
 (0)