Repository: nestjs/swagger
Pull request #3347 (2025-03-17 15:53:13)

Fix `selfRequired: false` not making an object-type property optional
## PR Checklist

Please check if your PR fulfills the following requirements:

- [x] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md
- [x] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)

## PR Type

What kind of change does this PR introduce?

- [x] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Other... Please describe:

## What is the current behavior?

Issue Number: N/A

When using `@ApiProperty({ type: 'object', selfRequired: false })` on an object (which is enforced by types, `required: false` cannot be used on an object as it refers to its own properties), the property is added to the list of `required` properties. This is a bug.

## What is the new behavior?

Setting `selfRequired: false` on a object-type property does work, and the property is not added to the list of `required`. This is implemented by having `selfRequired` taking precedence over `required`, as `selfRequired` is non-ambiguous about its intent (it can only be used for object types, with the clear meaning of applying to the decorated property).

## Does this PR introduce a breaking change?

- [ ] Yes
- [x] No

## Other information

Related to #3163.


Patch:
diff --git a/lib/services/schema-object-factory.ts b/lib/services/schema-object-factory.ts
index 5efa2305..076da32f 100644
--- a/lib/services/schema-object-factory.ts
+++ b/lib/services/schema-object-factory.ts
@@ -268,10 +268,10 @@ export class SchemaObjectFactory {
     };
 
     const typeDefinitionRequiredFields = propertiesWithType
-      .filter(
-        (property) =>
-          (property.required != false && !Array.isArray(property.required)) ||
-          ('selfRequired' in property && property.selfRequired != false)
+      .filter((property) =>
+        'selfRequired' in property
+          ? property.selfRequired != false
+          : property.required != false && !Array.isArray(property.required)
       )
       .map((property) => property.name);
 


Test patch:
diff --git a/e2e/api-spec.json b/e2e/api-spec.json
index cb89d3fa..248c7469 100644
--- a/e2e/api-spec.json
+++ b/e2e/api-spec.json
@@ -1463,6 +1463,23 @@
               }
             }
           },
+          "rawDefinition": {
+            "type": "object",
+            "properties": {
+              "name": {
+                "type": "string",
+                "example": "ErrorName"
+              },
+              "status": {
+                "type": "number",
+                "example": 400
+              }
+            },
+            "required": [
+              "name",
+              "status"
+            ]
+          },
           "enumWithDescription": {
             "enum": [
               "A",
@@ -1525,6 +1542,7 @@
           "createdAt",
           "urls",
           "options",
+          "rawDefinition",
           "enumWithDescription",
           "enum",
           "enumArr",
@@ -1600,6 +1618,12 @@
               "status"
             ]
           },
+          "optionalRawDefinition": {
+            "type": "object",
+            "additionalProperties": {
+              "type": "boolean"
+            }
+          },
           "enum": {
             "type": "string",
             "enum": [
diff --git a/e2e/src/cats/classes/cat.class.ts b/e2e/src/cats/classes/cat.class.ts
index 0c47fffd..793df6d1 100644
--- a/e2e/src/cats/classes/cat.class.ts
+++ b/e2e/src/cats/classes/cat.class.ts
@@ -60,7 +60,14 @@ export class Cat {
     required: ['name', 'status'],
     selfRequired: true
   })
-  rawDefinition?: Record<string, any>;
+  rawDefinition: Record<string, any>;
+
+  @ApiProperty({
+    type: 'object',
+    additionalProperties: { type: 'boolean' },
+    selfRequired: false
+  })
+  optionalRawDefinition?: Record<string, boolean>;
 
   @ApiProperty({
     enum: LettersEnum
diff --git a/e2e/src/cats/dto/create-cat.dto.ts b/e2e/src/cats/dto/create-cat.dto.ts
index c3bbc8fa..99506026 100644
--- a/e2e/src/cats/dto/create-cat.dto.ts
+++ b/e2e/src/cats/dto/create-cat.dto.ts
@@ -44,6 +44,23 @@ export class CreateCatDto {
   })
   readonly options?: Record<string, any>[];
 
+  @ApiProperty({
+    type: 'object',
+    properties: {
+      name: {
+        type: 'string',
+        example: 'ErrorName'
+      },
+      status: {
+        type: 'number',
+        example: 400
+      }
+    },
+    required: ['name', 'status'],
+    selfRequired: true
+  })
+  rawDefinition: Record<string, any>;
+
   @ApiProperty({
     description: 'Enum with description'
   })
diff --git a/e2e/validate-schema.e2e-spec.ts b/e2e/validate-schema.e2e-spec.ts
index 335d9fa8..628bc646 100644
--- a/e2e/validate-schema.e2e-spec.ts
+++ b/e2e/validate-schema.e2e-spec.ts
@@ -10,6 +10,7 @@ import {
   OpenAPIObject,
   SwaggerModule
 } from '../lib';
+import { SchemaObject } from '../lib/interfaces/open-api-spec.interface';
 import { ApplicationModule } from './src/app.module';
 import { Cat } from './src/cats/classes/cat.class';
 import { TagDto } from './src/cats/dto/tag.dto';
@@ -222,4 +223,11 @@ describe('Validate OpenAPI schema', () => {
       }
     });
   });
+
+  it('should not add optional properties to required list', async () => {
+    const document = SwaggerModule.createDocument(app, options);
+    const required = (document.components?.schemas?.Cat as SchemaObject)
+      ?.required;
+    expect(required).not.toContain('optionalRawDefinition');
+  });
 });