Skip to content

Commit 12759e2

Browse files
Merge pull request #153 from thibault-lr/feature/omit-schema-properties-declaration
Add a property hideDefinitions property for concealed Swagger documentation while retaining validation
2 parents 009118e + 469a22a commit 12759e2

File tree

2 files changed

+144
-36
lines changed

2 files changed

+144
-36
lines changed

packages/zod-openapi/src/lib/zod-openapi.spec.ts

+77
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,83 @@ describe('zodOpenapi', () => {
252252
});
253253
});
254254

255+
describe('Regarding omitted schema definitions', () => {
256+
it('supports schema hideDefinitions properties', () => {
257+
const zodSchema = extendApi(
258+
z
259+
.object({
260+
aArrayMin: z.array(z.string()).min(3),
261+
aArrayMax: z.array(z.number()).max(8),
262+
aArrayLength: z.array(z.boolean()).length(10),
263+
})
264+
.partial(),
265+
{
266+
description: 'I need arrays',
267+
hideDefinitions: ['aArrayLength'],
268+
}
269+
);
270+
271+
const apiSchema = generateSchema(zodSchema);
272+
273+
expect(apiSchema).toEqual({
274+
type: 'object',
275+
properties: {
276+
aArrayMin: {
277+
type: 'array',
278+
minItems: 3,
279+
items: { type: 'string' },
280+
},
281+
aArrayMax: {
282+
type: 'array',
283+
maxItems: 8,
284+
items: { type: 'number' },
285+
},
286+
},
287+
"hideDefinitions": ["aArrayLength"],
288+
description: 'I need arrays',
289+
});
290+
});
291+
292+
it('supports hideDefinitions on nested schemas', () => {
293+
const zodNestedSchema = extendApi(
294+
z.strictObject({
295+
aString: z.string(),
296+
aNumber: z.number().optional()
297+
}),
298+
{
299+
hideDefinitions: ['aNumber']
300+
}
301+
)
302+
const zodSchema = extendApi(
303+
z.object({ data: zodNestedSchema }).partial(),
304+
{
305+
description: 'I need arrays',
306+
}
307+
);
308+
const apiSchema = generateSchema(zodSchema);
309+
expect(apiSchema).toEqual({
310+
"description": "I need arrays",
311+
"properties": {
312+
"data": {
313+
"additionalProperties": false,
314+
"properties": {
315+
"aString": {
316+
"type": "string",
317+
},
318+
},
319+
"hideDefinitions": ["aNumber"],
320+
"required": [
321+
"aString",
322+
],
323+
"type": "object",
324+
},
325+
},
326+
"type": "object",
327+
}
328+
);
329+
});
330+
});
331+
255332
describe('record support', () => {
256333
describe('with a value type', () => {
257334
it('adds the value type to additionalProperties', () => {

packages/zod-openapi/src/lib/zod-openapi.ts

+67-36
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,48 @@
1-
import type {SchemaObject, SchemaObjectType} from 'openapi3-ts/oas31';
1+
import type { SchemaObject, SchemaObjectType } from 'openapi3-ts/oas31';
22
import merge from 'ts-deepmerge';
3-
import {AnyZodObject, z, ZodTypeAny} from 'zod';
3+
import { AnyZodObject, z, ZodTypeAny } from 'zod';
4+
5+
type AnatineSchemaObject = SchemaObject & { hideDefinitions?: string[] };
46

57
export interface OpenApiZodAny extends ZodTypeAny {
6-
metaOpenApi?: SchemaObject | SchemaObject[];
8+
metaOpenApi?: AnatineSchemaObject | AnatineSchemaObject[];
79
}
810

911
interface OpenApiZodAnyObject extends AnyZodObject {
10-
metaOpenApi?: SchemaObject | SchemaObject[];
12+
metaOpenApi?: AnatineSchemaObject | AnatineSchemaObject[];
1113
}
1214

1315
interface ParsingArgs<T> {
1416
zodRef: T;
15-
schemas: SchemaObject[];
17+
schemas: AnatineSchemaObject[];
1618
useOutput?: boolean;
19+
hideDefinitions?: string[];
1720
}
1821

1922
export function extendApi<T extends OpenApiZodAny>(
2023
schema: T,
21-
SchemaObject: SchemaObject = {}
24+
schemaObject: AnatineSchemaObject = {}
2225
): T {
23-
schema.metaOpenApi = Object.assign(schema.metaOpenApi || {}, SchemaObject);
26+
schema.metaOpenApi = Object.assign(schema.metaOpenApi || {}, schemaObject);
2427
return schema;
2528
}
2629

2730
function iterateZodObject({
2831
zodRef,
2932
useOutput,
33+
hideDefinitions,
3034
}: ParsingArgs<OpenApiZodAnyObject>) {
31-
return Object.keys(zodRef.shape).reduce(
32-
(carry, key) => ({
33-
...carry,
34-
[key]: generateSchema(zodRef.shape[key], useOutput),
35-
}),
36-
{} as Record<string, SchemaObject>
37-
);
35+
const reduced = Object.keys(zodRef.shape)
36+
.filter((key) => hideDefinitions?.includes(key) === false)
37+
.reduce(
38+
(carry, key) => ({
39+
...carry,
40+
[key]: generateSchema(zodRef.shape[key], useOutput),
41+
}),
42+
{} as Record<string, SchemaObject>
43+
);
44+
45+
return reduced;
3846
}
3947

4048
function parseTransformation({
@@ -54,16 +62,16 @@ function parseTransformation({
5462
['integer', 'number'].includes(`${input.type}`)
5563
? 0
5664
: 'string' === input.type
57-
? ''
58-
: 'boolean' === input.type
59-
? false
60-
: 'object' === input.type
61-
? {}
62-
: 'null' === input.type
63-
? null
64-
: 'array' === input.type
65-
? []
66-
: undefined,
65+
? ''
66+
: 'boolean' === input.type
67+
? false
68+
: 'object' === input.type
69+
? {}
70+
: 'null' === input.type
71+
? null
72+
: 'array' === input.type
73+
? []
74+
: undefined,
6775
{ addIssue: () => undefined, path: [] } // TODO: Discover if context is necessary here
6876
);
6977
} catch (e) {
@@ -77,8 +85,8 @@ function parseTransformation({
7785
...input,
7886
...(['number', 'string', 'boolean', 'null'].includes(output)
7987
? {
80-
type: output as 'number' | 'string' | 'boolean' | 'null',
81-
}
88+
type: output as 'number' | 'string' | 'boolean' | 'null',
89+
}
8290
: {}),
8391
},
8492
...schemas
@@ -165,10 +173,26 @@ function parseNumber({
165173
);
166174
}
167175

176+
177+
178+
function getExcludedDefinitionsFromSchema(schemas: AnatineSchemaObject[]): string[] {
179+
180+
181+
const excludedDefinitions = [];
182+
for (const schema of schemas) {
183+
if (Array.isArray(schema.hideDefinitions)) {
184+
excludedDefinitions.push(...schema.hideDefinitions)
185+
}
186+
}
187+
188+
return excludedDefinitions
189+
}
190+
168191
function parseObject({
169192
zodRef,
170193
schemas,
171194
useOutput,
195+
hideDefinitions,
172196
}: ParsingArgs<
173197
z.ZodObject<never, 'passthrough' | 'strict' | 'strip'>
174198
>): SchemaObject {
@@ -213,11 +237,13 @@ function parseObject({
213237
zodRef: zodRef as OpenApiZodAnyObject,
214238
schemas,
215239
useOutput,
240+
hideDefinitions: getExcludedDefinitionsFromSchema(schemas),
216241
}),
217242
...required,
218243
...additionalProperties,
244+
...hideDefinitions
219245
},
220-
zodRef.description ? {description: zodRef.description} : {},
246+
zodRef.description ? { description: zodRef.description, hideDefinitions } : {},
221247
...schemas
222248
);
223249
}
@@ -389,22 +415,27 @@ function parseUnion({
389415
useOutput,
390416
}: ParsingArgs<z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]>>): SchemaObject {
391417
const contents = zodRef._def.options;
392-
if (contents.reduce((prev, content) => prev && content._def.typeName === 'ZodLiteral', true)) {
418+
if (
419+
contents.reduce(
420+
(prev, content) => prev && content._def.typeName === 'ZodLiteral',
421+
true
422+
)
423+
) {
393424
// special case to transform unions of literals into enums
394425
const literals = contents as unknown as z.ZodLiteral<OpenApiZodAny>[];
395-
const type = literals
396-
.reduce((prev, content) =>
397-
!prev || prev === typeof content._def.value ?
398-
typeof content._def.value :
399-
null,
400-
null as null | string
401-
);
426+
const type = literals.reduce(
427+
(prev, content) =>
428+
!prev || prev === typeof content._def.value
429+
? typeof content._def.value
430+
: null,
431+
null as null | string
432+
);
402433

403434
if (type) {
404435
return merge(
405436
{
406437
type: type as 'string' | 'number' | 'boolean',
407-
enum: literals.map((literal) => literal._def.value)
438+
enum: literals.map((literal) => literal._def.value),
408439
},
409440
zodRef.description ? { description: zodRef.description } : {},
410441
...schemas

0 commit comments

Comments
 (0)