nestjs / swagger

OpenAPI (Swagger) module for Nest framework (node.js) :earth_americas:
https://nestjs.com
MIT License
1.7k stars 477 forks source link

Expose function to get JSON schema from object #2306

Open SmallhillCZ opened 1 year ago

SmallhillCZ commented 1 year ago

Is there an existing issue that is already proposing this?

Is your feature request related to a problem? Please describe it

This is a possible made up example, but there were many other times I could use the ability to get the JSON schema from an already annotated DTO object.

Specifying the type of AnotherObjectDto | false cannot be done to my knowledge easily without writing the JSON schema manually including the contents of AnotherObjectDto :

export class SomeObjectDto {
  @ApiProperty() value1!: number | string;
  @ApiProperty() value2!: number | string;
  @ApiPropertyOptional(/* THE UNION TYPE */) value3?: AnotherObjectDto | false;
}

class AnotherObjectDto {
  @ApiProperty() value1!: number | string;
  @ApiProperty() value2!: number | string;
}

Having an exported function for getting JSON schema we could do

@ApiPropertyOptional({ oneOf: [ getJsonSchema(AnotherObjectDto), {type: "boolean", enum: [ false ] } ]})
value3?: AnotherObjectDto | false;

Describe the solution you'd like

Workaround is importing parts from @nestjs/swagger and using:


function getJsonSchema(targetConstructor: Type<unknown>){
  const factory = new SchemaObjectFactory(new ModelPropertiesAccessor(), new SwaggerTypesMapper());

  const schemas: Record<string, SchemaObject> = {};
  this.factory.exploreModelSchema(targetConstructor, schemas);

  return schemas[targetConstructor.name];
}

Teachability, documentation, adoption, migration strategy

No response

What is the motivation / use case for changing the behavior?

Being able to use the JSON schema of the decorated object either in custom types in @ApiProperty decorator or elsewhere.

muka commented 7 months ago

That would be very useful to mix @Body and @UploadedFile

An example implementation, in controller

  // ...
  @ApiUpload(MyAssetDto, 'file')
  @ApiConsumes('multipart/form-data')
  @UseInterceptors(FileInterceptor('file'))
  saveAsset(
    @UploadedFile() file: Express.Multer.File,
    @Body() data: MyAssetDto,
  ): Promise<void> {
    // ...
  }

In a upload-decorator.ts

import { Type, applyDecorators } from '@nestjs/common';
import {
  ApiBody,
  ApiBodyOptions,
  ApiConsumes,
  ApiExtraModels,
  ApiOkResponse,
} from '@nestjs/swagger';
import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';

import { ModelPropertiesAccessor } from '@nestjs/swagger/dist/services/model-properties-accessor';
import { SchemaObjectFactory } from '@nestjs/swagger/dist/services/schema-object-factory';
import { SwaggerTypesMapper } from '@nestjs/swagger/dist/services/swagger-types-mapper';

function getJsonSchema(targetConstructor: Type<unknown>) {
  const factory = new SchemaObjectFactory(
    new ModelPropertiesAccessor(),
    new SwaggerTypesMapper(),
  );

  const schemas: Record<string, SchemaObject> = {};
  factory.exploreModelSchema(targetConstructor, schemas);

  return schemas[targetConstructor.name];
}

const createBodySchema = (body?: Type, fileField = 'file'): ApiBodyOptions => {
  const objSchema: SchemaObject = body ? getJsonSchema(body) : undefined;
  return {
    schema: {
      type: 'object',
      properties: {
        ...(objSchema?.properties ? objSchema?.properties : {}),
        [fileField]: {
          type: 'file',
        },
      },
    },
  };
};

export function ApiUpload(body?: Type, fileField?: string) {
  const extraModels = body ? [body] : [];
  return applyDecorators(
    ApiOkResponse(),
    ApiBody(createBodySchema(body, fileField)),
    ApiExtraModels(...extraModels),
  );
}

Results in something like

image