Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

Spring Boot で OpenAPI の discriminator を使いたい!

こんにちは。エンジニアの松下です。
最近は龍が如く 8 をクリアし、 FF7 リバースを進めているところです。
MP 消費なしで属性攻撃できるようになってて快適!

今回は Spring Boot で OpenAPI ドキュメントを自動生成するときの小技を紹介します。
また、 JSON ライブラリは Kotlin Serialization ドキュメント生成は springdoc-openapi を使うとします。

例えば以下の様な JSON があったとき。

[
  {
    "type": "music",
    "id": "music1234",
    "url": "xxxxx/audio.mp3",
    "artist": "hoge"
  },
  {
    "type": "photo",
    "id": "photo1234",
    "url": "xxxxx/photo.jpg",
    "description": "例の画像です。"
  },
  {
    "type": "movie",
    "id": "movie1234",
    "url": "xxxxx/movie.mp4",
    "description": "例の動画です。",
    "duration": 90
  }
]

モデル側は以下の様になると思います。

sealed interface Content

data class Music(
  val id: String,
  val url: String,
  val artist: String,
): Content

data class Photo(
  val id: String,
  val url: String,
  val description: String,
): Content

data class Movie(
  val id: String,
  val url: String,
  val description: String,
  val duration: Int,
): Content

Kotlin Serialization の場合は @JsonClassDiscriminator を使うことで JSON を出力すること自体は可能です。
kotlinlang.org

@JsonClassDiscriminator("type")
@Serializable
sealed interface Content

@SerialName("music")
@Serializable
data class Music(
  val id: String,
  val url: String,
  val artist: String,
): Content

@SerialName("photo")
@Serializable
data class Photo(
  val id: String,
  val url: String,
  val description: String,
): Content

@SerialName("movie")
@Serializable
data class Movie(
  val id: String,
  val url: String,
  val description: String,
  val duration: Int,
): Content

これに springdoc-openapi のアノテーションを以下のように付与すると表現できます!

@Schema(
    discriminatorProperty = "type",
    discriminatorMapping = [
        DiscriminatorMapping(value = "music", schema = Music::class),
        DiscriminatorMapping(value = "photo", schema = Photo:class),
        DiscriminatorMapping(value = "movie", schema = Movie:class),
    ],
    oneOf = [
        Music::class,
        Photo::class,
        Movie::class,
    ],
)
@JsonClassDiscriminator("type")
@Serializable
sealed interface Content

@SerialName("music")
@Serializable
data class Music(
  val id: String,
  val url: String,
  val artist: String,
): Content

@SerialName("photo")
@Serializable
data class Photo(
  val id: String,
  val url: String,
  val description: String,
): Content

@SerialName("movie")
@Serializable
data class Movie(
  val id: String,
  val url: String,
  val description: String,
  val duration: Int,
): Content

生成されるドキュメントのイメージは以下です。

Content:
  type: object
  discriminator:
    propertyName: type
    mapping:
      music: '#/components/schemas/Music'
      photo: '#/components/schemas/Photo'
      movie: '#/components/schemas/Movie'
  oneOf:
  - $ref: '#/components/schemas/Music'
  - $ref: '#/components/schemas/Photo'
  - $ref: '#/components/schemas/Movie'

これでクライアントサイドチームが OpenAPI からコード自動生成をしていても、意図どおりのコードが生成されます!
備考: swagger.io

最後に

弊社ではいっしょに働くアプリエンジニアを募集しています!
ご興味のある方はぜひご応募いただけますと嬉しいです。

Androidエンジニア募集中!: www.wantedly.com
リードエンジニア(AndroidiOS問わず)も募集中!:www.wantedly.com