nestjs / swagger

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

ApiProperty for unknown keys (dictionaries) #1771

Open mxlpitelik opened 2 years ago

mxlpitelik commented 2 years ago

Is there an existing issue that is already proposing this?

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

If my schema is contain dictionaries (properties with unknown key) I dont know how create schema descriptions for it like here https://swagger.io/docs/specification/data-models/dictionaries/

Example:

class ColorDto {
  @ApiProperty()
  hex: string;

  @ApiProperty()
  rgb: string;
}

class RainbowDto {
  @ApiProperty()
  name: string;

  @???????
  colors: { [key: string] : ColorDto }
}

Describe the solution you'd like

class ColorDto {
  @ApiProperty()
  hex: string;

  @ApiProperty()
  rgb: string;
}

class RainbowDto {
  @ApiProperty()
  name: string;

  @ApiAdditionalProperty({
     type: ColorDto
  })
  colors: { [key: string] : ColorDto }
}

Teachability, documentation, adoption, migration strategy

No response

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

This is super often problem, and I dont know how to solve this trouble for now...

amitzig commented 2 years ago

+1 Any update on this matter?

asherifhegazy commented 2 years ago

Any update ?

madmxg commented 2 years ago

Workaround

@Controller('api/v1/some-resource')
export class SomeResourceController {
  @ApiOkResponse({
    description: 'Some description.',
    schema: {
      type: 'object',
      example: {
        'key1': {
          name: 'string',
          date: 'string',
        },
        'key2': {
          name: 'string',
          date: 'string',
        },
      },
      additionalProperties: {
        $ref: '#/components/schemas/NameOfSomeDto',
      },
    },
  })
  public async readSomeResource() {}
}
maxfriedmann commented 2 years ago

Also works via @ApiProperty({additionalProperties: {$ref: "#/components/schemas/NameOfSomeDto"}});

voidberg1 commented 2 years ago

Any updates on this? Unfortunately, it seems decorators cannot be attached to index signatures. :(

ouzkhanmete commented 1 year ago
import { v4 } from 'uuid'

class SomeNestedDTO {
 @ApiProperty({ example: 'abc' })
 a: string

 @ApiProperty({ example: 322 })
 b: number
}

@ApiExtraModels(SomeNestedDTO)
class MainDTO {
  @ApiProperty({ 
    type: 'object',
    properties: { [v4()]: { $ref: '#/components/schemas/SomeNestedDTO' } }
  })
  someNestedDTOMap: Record<string, SomeNestedDTO> 
}

The result should look like:

{
  "someNestedDTOMap": {
    "d9472a82-8c2c-4f8b-a1cc-8d22f8929bd3": {
      "a": "abc",
      "b": 322
    }
  }
}
iiian commented 1 year ago

Use Record<string, T>. If you're pre-4.0 you're probably SOL.

export class MyDto {
  @ApiProperty({ additionalProperties: { type: 'string' } });
  bag_of_fun: Record<string, string>;
}
deyceg commented 1 year ago

+1

It makes these properties optional and generating mock data against openapi specification is then incorrect.

Kibyra commented 1 year ago

What about this solution?

import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger';
class ColorDto {
  @ApiProperty()
  hex: string;

  @ApiProperty()
  rgb: string;
}

@ApiExtraModels(ColorDto)
class RainbowDto {
  @ApiProperty()
  name: string;

  @ApiProperty({
    type: 'object',
    additionalProperties: { $ref: getSchemaPath(ColorDto) },
  })
  colors: Record<string, ColorDto>;
}

The result should look like:

{
  "name": "string",
  "colors": {
    "additionalProp1": {
      "hex": "string",
      "rgb": "string"
    },
    "additionalProp2": {
      "hex": "string",
      "rgb": "string"
    },
    "additionalProp3": {
      "hex": "string",
      "rgb": "string"
    }
  }
}
kervral commented 10 months ago

While @Kibyra answer works for nested properties, we still can't declare class level indexed properties as swagger definition like this:

import { ApiProperty } from '@nestjs/swagger';

class ColorDto {
  // Can't apply decorator here
  [key: string]: unknown; 

  @ApiProperty()
  hex: string;

  @ApiProperty()
  rgb: string;
}

Can a class decorator could be considered allowing to add additional properties to the class ?

Ethan199412 commented 10 months ago

ApiPropertyOptional could only help you make one layer, which means it could only solve Record<string, Dto>, but it can't solve Record<string, Record<string, Dto>>

mwaeckerlin commented 3 months ago

While @Kibyra answer works for nested properties, we still can't declare class level indexed properties as swagger definition like this:

class ColorDto {
  // Can't apply decorator here
  [key: string]: unknown; 
}

I'm having the same issue.

That's a typical use case: Have an object as parameter with a unique id as key and that maps to some content.

I haven't found a proper solution for this.

Example:

  WalletAddress?: string
  Memo?: string
  DestinationTag?: string
}

export class CryptoAddresses {
  // cannot add @ApiProperty(…) → decorators are not valid here
  [id: string]: CryptoAddress
}

I can then create a controller:

  ...
  public addresses: CryptoAddresses = {}
  @ApiExtraModels(CryptoAddress)
  @Get('addresses') @ApiResponse({status: 200, schema: {type: 'object', additionalProperties: {$ref: getSchemaPath(CryptoAddress)}}}) getAddresses(): CryptoAddresses {
    return this.addresses
  }

But I can't get a schema for CryptoAddresses to be created…

For me, it's important to get a proper schema definition, because the client generates it's types from the schema file.

Any idea / solution for this?

A possible solution would be to add something to create an arbitrary schema for an arbitrary type to solve all unhandled use cases, such as:

@ApiSchema({
    type: object,
    additionalProperties: $ref: getSchemaPath(CryptoAddress)
})
class CryptoAddresses {...}
GHkrishna commented 1 month ago

@Kibyra gave a great solution to this.

Just a followup question. Can we further add type checking to this using the @Type decorator to the property.

I think this can be tackled using a custom validator. But is this achievable using just the @Type decorator? Or has anyone tried with something else.

muka commented 1 month ago

I used this for a generic object with some optional typed fieds

class MyOptions {
  [key: string]: any
  @ApiPropertyOptional()
  someTypeField?: boolean
  // ...
}
@ApiExtraModels(MyOptions)

//...
@ApiPropertyOptional({
    type: 'object',
    allOf: [
      { $ref: getSchemaPath(MyOptions) },
      {
        type: 'object',
      },
    ],
  })
  options?: MyOptions;

generates in

options?: MyOptions & Record<string, unknown>;

decorator


const apiGenericObject = (required: boolean, models: Function[]) => { // eslint-disable-line
  return applyDecorators(
    ApiProperty({
      required,
      type: 'object',
      allOf: [
        ...(models && models.length
          ? models.map((m) => ({ $ref: getSchemaPath(m) }))
          : []),
        {
          type: 'object',
        },
      ],
    }),
  );
};

export function ApiGenericProperty(...models: Function[]) {  // eslint-disable-line
  return apiGenericObject(true, models);
}
export function ApiGenericPropertyOptional(...models: Function[]) {  // eslint-disable-line
  return apiGenericObject(false, models);
}

usage

@ApiExtraModels(MyOptions)
class MyObject {
  @ApiGenericPropertyOptional(MyOptions)
  options?: MyOptions
}