OpenAPI discriminator

신연우·2025년 1월 23일

WIL

목록 보기
19/21

문제 상황

OpenAPI에서 지원하는 oneOf 기능을 사용해서 API dto를 정의해서 사용하고 싶은 때가 있습니다.

# 예시
ExampleSearchConditionDto:
  type: object
  oneOf:
    - $ref: '#/components/schemas/ExampleSearchByNameDto'
    - $ref: '#/components/schemas/ExampleSearchByPhoneNumberDto'

ExampleSearchByNameDto:
  type: object
  required:
    - name
  properties:
    name:
      type: string

ExampleSearchByPhoneNumberDto:
  type: object
  required:
    - phoneNumber
  properties:
    phoneNumber:
      type: string

그러나 프로젝트에서 위와 같이 oneOf dto를 정의한 뒤 Kotlin 코드로 codegen 하면 결과물이 기대한 것과는 조금 다르게 나왔습니다.

// codegen 된 dto
data class ExampleSearchConditionDto (
    @field:JsonProperty("name")
    val name: kotlin.String,

    @field:JsonProperty("phoneNumber")
    val phoneNumber: kotlin.String
)

namephoneNumber 프로퍼티가 하나의 클래스에서 모두 필수값으로 정의가 됩니다. 따라서 API 스펙대로 name만 json에 담아서 보냈을 때 서버에서는 오류가 발생하게 됩니다.

discriminator

openapi에서 제공하는 discriminator를 이용하면 해당 문제를 해결할 수 있습니다.

ExampleSearchConditionDto:
  type: object
  required:
    - searchType
  properties:
    searchType:
      $ref: '#/components/schemas/ExampleSearchTypeEnum'
  discriminator:
    propertyName: searchType

ExampleSearchTypeEnum:
  type: string
  enum:
    - NAME
    - PHONE_NUMBER

위와 같이 oneOf에서 사용할 dto를 식별할 수 있는 값을 enum으로 정의해줍니다. 그리고 이 enum을 필드로 가지는 dto를 하나 만들어줍니다. 이 dto는 kotlin의 interface라고 이해하시면 됩니다.

ExampleSearchByNameDto:
  type: object
  allOf:
    - $ref: '#/components/schemas/ExampleSearchConditionDto'
    - type: object
      required:
        - name
      properties:
        name:
          type: string

ExampleSearchByPhoneNumberDto:
  type: object
  allOf:
    - $ref: '#/components/schemas/ExampleSearchConditionDto'
    - type: object
      required:
        - phoneNumber
      properties:
        phoneNumber:
          type: string

oneOf로 사용할 각 dto를 정의합니다. 이때, 방금 정의한 interface 역할을 하는 dto를 allOf로 결합합니다.

ExampleSearchConditionDto:
  type: object
  required:
    - searchType
  properties:
    searchType:
      $ref: '#/components/schemas/ExampleSearchTypeEnum'
  discriminator:
    propertyName: searchType
    mapping:
      NAME: '#/components/schemas/ExampleSearchByNameDto'
      PHONE_NUMBER: '#/components/schemas/ExampleSearchByPhoneNumberDto'

마지막으로 sealed interface dto에 mapping을 선언합니다. mappingdiscriminator.propertyName에 설정된 프로퍼티에 들어오는 값에 따라 어떤 dto로 해석할지를 결정해줍니다.

기본값은 dto의 이름을 key로 하지만, 이번 예시에서는 enum을 직접 만들어서 key로 사용하기 때문에 enum 이름을 key로 설정해줍니다.

위에서 선언한 dto를 kotlin으로 codegen하면 아래와 같은 결과물을 얻게 됩니다.

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "statisticsSearchType", visible = true)
@JsonSubTypes(
    JsonSubTypes.Type(value = ExampleSearchByNameDto::class, name = "NAME"),
    JsonSubTypes.Type(value = ExampleSearchByPhoneNumberDto::class, name = "PHONE_NUMBER")
)

interface ExampleSearchConditionDto {

    @get:JsonProperty("searchType")
    val searchType: ExampleSearchTypeEnum
}

interface 역할을 한다고 설명한 dto는 interface로 codegen됩니다. @JsonTypeInfo, @JsonSubTypes 어노테이션이 달려있어 요청으로 들어온 json의 형태에 따라 각 구현체로 파싱될 수 있습니다.

data class ExampleSearchByNameDto (

    @field:JsonProperty("searchType")
    override val searchType: ExampleSearchTypeEnum,
    
    @field:JsonProperty("name")
    override val name: String,
) : ExampleSearchConditionDto

data class ExampleSearchByPhoneNumberDto (

    @field:JsonProperty("searchType")
    override val searchType: ExampleSearchTypeEnum,
    
    @field:JsonProperty("phoneNumber")
    override val phoneNumber: String,
) : ExampleSearchConditionDto

oneOf로 사용하고 싶던 각 dto는 모두 interface를 구현하는 식으로 codegen됩니다.

단점

swagger UI에서 request body로 oneOf dto 중 하나를 사용해야 한다는것을 인지할 수 없다는 단점이 있습니다.

profile
남들과 함께하기 위해서는 혼자 나아갈 수 있는 힘이 있어야 한다.

0개의 댓글