(Kotlin)Swaggerをコードから生成してくれるSpringFoxが便利だという話


この記事の目的

SpringFoxはいいぞ、ということを布教するための記事です。
知らない人のための入り口になればと思います。。。

SpringFoxとは?

Springで構築された実際のAPIからSwagger形式で仕様書を自動生成してくれるツールです。
具体的に言うと、Swaggerでよく使うyamlを書くのではなく、
JavaやKotlinで実装されたインタフェースから自動で生成することになります。

何がいいの?

Swaggerは結局yamlを地道に書いていくしかないです。
APIを実装していく中で変更があったとき、yamlと実装を変更する必要が出てきます。
つまり、結局は二重管理となりつらくなる。。。
そういった苦しみから解放してくれます。

使用上の注意

記事中にも記載しますが、
SpringFoxで仕様について文章を長々と書くと、当然行数も伸びます。
記載は最小限として、これとは別に社内ドキュメントに細かい仕様を記載して行った方が良いかもしれません。

セットアップ

記事が長大となってしまうので、公式を見ながら設定してください。。(後で書く)

記載方法

package com.example.controller

import io.swagger.annotations.Api
import io.swagger.annotations.ApiModel
import io.swagger.annotations.ApiModelProperty
import io.swagger.annotations.ApiOperation
import io.swagger.annotations.ApiParam
import io.swagger.annotations.ApiResponse
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.time.LocalDateTime

// tagはAPIをグループ分けするときに使うのが便利です
// descriptonにController全体(つまり機能)の説明を記載します
@Api(tags = ["tagA", "tagB"], description = "これはサンプルAPIです")
// 以下2つはSpringのアノテーション
@RestController
@RequestMapping("example")
class ExampleController {
    // search_itemのAPIに関する説明を記載します
    @ApiOperation(
        "アイテム検索API",
        notes = "第1ソート: updated_at昇順\n" // 改行コードを入れれば実際に改行される
                + "第2ソート:item_id昇順" // この説明が増えると実装時に見辛くなる....
    )
    // httpStatusコードとそのメッセージを記載します
    @ApiResponse(code = 200, message ="成功時メッセージ")
    @GetMapping("/search_item", produces = [MediaType.APPLICATION_JSON_VALUE])
    fun searchItem(
        // APIにわたすパラメータの説明/パラメータ例を記載します。
        @ApiParam(value = "検索文字列", example = "exampleパラメータ")
        // これはSpringのアノテーション
        @RequestParam(required = true)
        keyword: String
    ): ResponseEntity<ItemListResponseBody> {
        return ResponseEntity(search(keyword), HttpStatus.OK)
    }
    // ダミーの値を返すための関数
    fun search(keyword: String): ItemListResponseBody {
        val itemList = listOf<Item>(
            Item(1, "Name1", LocalDateTime.now(), LocalDateTime.now()),
            Item(2, "Name2", LocalDateTime.now(), LocalDateTime.now())
        )
        return ItemListResponseBody(itemList)
    }
}

// ここから下がレスポンスボディの定義になります
// レスポンスボディ全体の説明が以下
@ApiModel(description = "検索結果のレスポンスボディ")
data class ItemListResponseBody(val itemList: List<Item>)

data class Item(
    // レスポンスボディ内の各パラメータの説明を記述します
    @ApiModelProperty(value = "アイテムId", example = "1")
    val id: Int,

    @ApiModelProperty(value = "アイテムの名前", example = "名称例1")
    val name: String,

    @ApiModelProperty(value = "更新日", example = "2019-09-20 20:00:00")
    val updatedAt: LocalDateTime,

    @ApiModelProperty(value = "作成日", example = "2019-09-15 12:00:00")
    val createdAt: LocalDateTime
)

Swagger UIを見ると、こんな感じで生成されます。

Open API Generator等と連携するためのモノは

http://(ホスト)/api-docsに格納されています。
今回の例だと以下のように生成されています。
人間が読むものではないので、見づらい。。

{"swagger":"2.0","info":{"description":"Api Documentation","version":"1.0","title":"Api Documentation","termsOfService":"urn:tos","contact":{},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0"}},"host":"localhost:8080","basePath":"/","tags":[{"name":"tagA","description":"これはサンプルAPIです"},{"name":"tagB","description":"これはサンプルAPIです"}],"paths":{"/example/search_item":{"get":{"tags":["tagA","tagB"],"summary":"アイテム検索API","description":"第1ソート: updated_at昇順\n第2ソート:item_id昇順","operationId":"searchItemUsingGET","produces":["application/json"],"parameters":[{"name":"keyword","in":"query","description":"検索文字列","required":false,"type":"string","allowEmptyValue":false,"x-example":"exampleパラメータ"}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/ItemListResponseBody"}}},"deprecated":false}}},"definitions":{"Item":{"type":"object","properties":{"createdAt":{"type":"string","format":"date-time","example":"2019-09-15 12:00:00","description":"作成日"},"id":{"type":"integer","format":"int32","example":1,"description":"アイテムId"},"name":{"type":"string","example":"名称例1","description":"アイテムの名前"},"updatedAt":{"type":"string","format":"date-time","example":"2019-09-20 20:00:00","description":"更新日"}},"title":"Item"},"ItemListResponseBody":{"type":"object","required":["itemList"],"properties":{"itemList":{"type":"array","items":{"$ref":"#/definitions/Item"}}},"title":"ItemListResponseBody","description":"検索結果のレスポンスボディ"}}}