Skip to content

Commit 82ad30c

Browse files
authored
fix: remove optional object properties from required array when using @nestjs/swagger v7 (#371)
1 parent 43ad423 commit 82ad30c

5 files changed

Lines changed: 132 additions & 3 deletions

File tree

packages/nestjs-zod/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"@nestjs/common": "^11.1.5",
6565
"@nestjs/core": "^11.1.5",
6666
"@nestjs/swagger": "^11.2.0",
67+
"@nestjs/swagger-v7": "npm:@nestjs/swagger@7.4.2",
6768
"@nestjs/testing": "^11.1.12",
6869
"@stoplight/spectral-core": "^1.22.0",
6970
"@stoplight/spectral-rulesets": "^1.22.1",

packages/nestjs-zod/src/__e2e_tests__/openapi.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import z from 'zod/v4';
2121
import { createZodDto } from '../dto';
2222
import { SwaggerModule } from '@nestjs/swagger';
23+
import { SwaggerModule as SwaggerModuleV7 } from '@nestjs/swagger-v7';
2324
import { cleanupOpenApiDoc } from '../cleanupOpenApiDoc';
2425
import get from 'lodash/get';
2526
import { PREFIX } from '../const';
@@ -3122,6 +3123,44 @@ describe('issue#368', () => {
31223123
});
31233124
});
31243125

3126+
describe('issue#371 - optional object properties in @nestjs/swagger version 7', () => {
3127+
test('does not include optional object as a required field', async () => {
3128+
class BodyDto extends createZodDto(
3129+
z.object({
3130+
name: z.string(),
3131+
filter: z
3132+
.object({
3133+
age: z.number(),
3134+
})
3135+
.optional(),
3136+
}),
3137+
) {}
3138+
3139+
@Controller()
3140+
class TestController {
3141+
constructor() {}
3142+
3143+
@Post()
3144+
create(@Body() _body: BodyDto) {
3145+
return {};
3146+
}
3147+
}
3148+
3149+
const doc = await getSwaggerDoc(TestController, {
3150+
swaggerVersion: '7',
3151+
});
3152+
3153+
expect(get(doc, 'components.schemas.BodyDto.required')).toEqual(['name']);
3154+
expect(
3155+
get(doc, 'components.schemas.BodyDto.properties.filter'),
3156+
).not.toHaveProperty('selfRequired');
3157+
expect(JSON.stringify(doc)).not.toContain(PREFIX);
3158+
3159+
expect(await getOpenApiErrors(doc, '3.0')).toHaveLength(0);
3160+
expect(await getOpenApiErrors(doc, '3.1')).toHaveLength(0);
3161+
});
3162+
});
3163+
31253164
async function createApp(controllerClass: Type<unknown>) {
31263165
@Module({
31273166
imports: [],
@@ -3142,15 +3181,20 @@ async function getSwaggerDoc(
31423181
{
31433182
cleanUp = true,
31443183
version,
3184+
swaggerVersion,
31453185
}: {
31463186
cleanUp?: boolean;
31473187
version?: '3.1' | '3.0' | 'auto';
3188+
swaggerVersion?: '7' | 'default';
31483189
} = {},
31493190
) {
31503191
const app = await createApp(controllerClass);
31513192

3152-
const doc = SwaggerModule.createDocument(app, new DocumentBuilder().build());
3193+
const doc = (
3194+
swaggerVersion === '7' ? SwaggerModuleV7 : SwaggerModule
3195+
).createDocument(app, new DocumentBuilder().build());
31533196
if (cleanUp) {
3197+
// @ts-expect-error - FIXME
31543198
return cleanupOpenApiDoc(doc, { version });
31553199
} else {
31563200
return doc;

packages/nestjs-zod/src/cleanupOpenApiDoc.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,11 +309,29 @@ function cleanupSchema({
309309
oldOpenapiSchema,
310310
);
311311

312-
for (const propertySchema of Object.values(
312+
for (const [propertyName, propertySchema] of Object.entries(
313313
newOpenapiSchema.properties || {},
314314
)) {
315315
if (SELF_REQUIRED_KEY in propertySchema) {
316+
// @nestjs/swagger v7 has a bug where `required` is not set properly when
317+
// one of the parameters is an optional object
318+
if (
319+
!propertySchema[SELF_REQUIRED_KEY] &&
320+
Array.isArray(newOpenapiSchema.required)
321+
) {
322+
const idx = newOpenapiSchema.required.indexOf(propertyName);
323+
if (idx !== -1) {
324+
newOpenapiSchema.required.splice(idx, 1);
325+
}
326+
if (newOpenapiSchema.required.length === 0) {
327+
delete newOpenapiSchema.required;
328+
}
329+
}
316330
delete propertySchema[SELF_REQUIRED_KEY];
331+
332+
if ('selfRequired' in propertySchema) {
333+
delete propertySchema.selfRequired;
334+
}
317335
}
318336

319337
if (USES_THREE_POINT_ONE_SYNTAX_KEY in propertySchema) {

packages/nestjs-zod/src/const.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ export const PARENT_ID_KEY = `${PREFIX}-parent-id`;
55
export const PARENT_ADDITIONAL_PROPERTIES_KEY = `${PREFIX}-parent-additional-properties`;
66
export const PARENT_HAS_REFS_KEY = `${PREFIX}-parent-has-refs`;
77
export const UNWRAP_ROOT_KEY = `${PREFIX}-unwrap-root`;
8-
export const USES_THREE_POINT_ONE_SYNTAX_KEY = `${PREFIX}-uses-3.1-syntax`;
8+
export const USES_THREE_POINT_ONE_SYNTAX_KEY = `${PREFIX}-uses-3-point-1-syntax`;
99
export const SELF_REQUIRED_KEY = `${PREFIX}-self-required`;
1010
export const PARENT_METADATA_KEY = `${PREFIX}-parent-metadata`;

pnpm-lock.yaml

Lines changed: 66 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)