diff --git a/lib/decorators/api-property.decorator.ts b/lib/decorators/api-property.decorator.ts index 11ad63557..07a34387b 100644 --- a/lib/decorators/api-property.decorator.ts +++ b/lib/decorators/api-property.decorator.ts @@ -5,6 +5,7 @@ import { EnumAllowedTypes, SchemaObjectMetadata } from '../interfaces/schema-object-metadata.interface'; +import { ParameterStyle } from '../interfaces/open-api-spec.interface'; import { getEnumType, getEnumValues } from '../utils/enum.utils'; import { createPropertyDecorator, getTypeIsArrayTuple } from './helpers'; @@ -20,6 +21,19 @@ export type ApiPropertyCommonOptions = SchemaObjectMetadata & { * @see [Swagger link objects](https://swagger.io/docs/specification/links/) */ link?: () => Type | Function; + /** + * Describes how the parameter value will be serialized. + * + * @see [Swagger serialization](https://swagger.io/docs/specification/serialization/) + */ + style?: ParameterStyle; + /** + * When true, array or object parameter values generate + * separate parameters for each value. + * + * @see [Swagger serialization](https://swagger.io/docs/specification/serialization/) + */ + explode?: boolean; }; export type ApiPropertyOptions = diff --git a/test/explorer/swagger-explorer.spec.ts b/test/explorer/swagger-explorer.spec.ts index 3ff4d0bcc..2850038b3 100644 --- a/test/explorer/swagger-explorer.spec.ts +++ b/test/explorer/swagger-explorer.spec.ts @@ -2615,4 +2615,77 @@ describe('SwaggerExplorer', () => { GlobalParametersStorage.clear(); }); }); + + describe('deepObject style for nested query params', () => { + class GeolocationDto { + @ApiProperty() + latitude: number; + + @ApiProperty() + longitude: number; + + @ApiProperty({ description: 'Distance in kilometers' }) + distance: number; + } + + class SearchQueryDto { + @ApiProperty({ + required: false, + type: () => GeolocationDto, + style: 'deepObject', + explode: true + }) + geolocation?: GeolocationDto; + } + + @Controller('search') + class SearchController { + @Get() + @ApiOperation({ summary: 'Search' }) + search(@Query() query: SearchQueryDto): void {} + } + + it('should emit a single query parameter with style=deepObject and $ref schema', () => { + const explorer = new SwaggerExplorer(schemaObjectFactory); + const routes = explorer.exploreController( + { + instance: new SearchController(), + metatype: SearchController + } as InstanceWrapper, + new ApplicationConfig(), + { modulePath: '' } + ); + + const params = routes[0].root.parameters; + expect(params).toHaveLength(1); + expect(params[0]).toMatchObject({ + name: 'geolocation', + in: 'query', + required: false, + style: 'deepObject', + explode: true, + schema: { + $ref: '#/components/schemas/GeolocationDto' + } + }); + }); + + it('should not flatten GeolocationDto properties into individual query params', () => { + const explorer = new SwaggerExplorer(schemaObjectFactory); + const routes = explorer.exploreController( + { + instance: new SearchController(), + metatype: SearchController + } as InstanceWrapper, + new ApplicationConfig(), + { modulePath: '' } + ); + + const params = routes[0].root.parameters; + const paramNames = params.map((p: any) => p.name); + expect(paramNames).not.toContain('latitude'); + expect(paramNames).not.toContain('longitude'); + expect(paramNames).not.toContain('distance'); + }); + }); }); diff --git a/test/services/schema-object-factory.spec.ts b/test/services/schema-object-factory.spec.ts index 371a8713d..1169d39d1 100644 --- a/test/services/schema-object-factory.spec.ts +++ b/test/services/schema-object-factory.spec.ts @@ -692,6 +692,77 @@ describe('SchemaObjectFactory', () => { }); }); + describe('createFromModel (deepObject)', () => { + class GeolocationDto { + @ApiProperty() + latitude: number; + + @ApiProperty() + longitude: number; + + @ApiProperty({ description: 'Distance in kilometers' }) + distance: number; + } + + it('should preserve style and explode at ParameterObject level for named nested query params', () => { + class QueryDto { + @ApiProperty({ + required: false, + type: () => GeolocationDto, + style: 'deepObject', + explode: true + }) + geolocation?: GeolocationDto; + } + + const schemas = {}; + const param = { + name: 'geolocation', + in: 'query', + required: false, + type: GeolocationDto, + style: 'deepObject', + explode: true + }; + + const [result] = schemaObjectFactory.createFromModel( + [param as any], + schemas + ); + + const mapped = swaggerTypesMapper.mapParamTypes([result as any]); + expect(mapped[0]).toMatchObject({ + name: 'geolocation', + in: 'query', + style: 'deepObject', + explode: true, + schema: { + $ref: '#/components/schemas/GeolocationDto' + } + }); + }); + + it('should not flatten nested object properties when style is deepObject', () => { + const schemas = {}; + const param = { + name: 'geolocation', + in: 'query', + required: false, + type: GeolocationDto, + style: 'deepObject', + explode: true + }; + + const result = schemaObjectFactory.createFromModel( + [param as any], + schemas + ); + + // Should produce a single parameter (not 3 flattened ones) + expect(result).toHaveLength(1); + }); + }); + describe('createEnumSchemaType', () => { it('should assign schema type correctly if enumName is provided', () => { const metadata = {