SpringFoxからspringdoc-openapiに移行してみた

SpringFoxに不満があったので、springdoc-openapiに移行してみたお話。

- [SpringFox](https://springfox.github.io/springfox/)
- [springdoc-openapi](https://springdoc.org/)

## なにこれ

そもそも、SpringFoxとかspringdoc-openapiってなんなのかというと、Spring BootでREST APIなどを作るときに、コントローラやモデルにアノテーションをつけてあげると、いい感じにOpenAPIドキュメントを生成してくれて、ついでにSwagger UIも立ち上げてくれるやつです。

SpringでSwaggerというとSpringFoxがメジャーな感じなのですが、OAS 3.0(OpenAPI Specification 3.0)に対応していないのと、メンテが鈍ってきてる感じが否めないですね。

そんな中、突如現れたspringdoc-openapiは、そういったSpringFoxへの不満から生み出されたライブラリで、OAS 3.0に対応していることはもちろん、WebFluxのようなSpringの新機能にも対応しているというのが特徴のようです。

## 移行方法

公式に[Migrating from SpringFox](https://springdoc.org/migrating-from-springfox.html)というページがあるので、そのとおりにやるだけ……なんですが、ドキュメントが簡素すぎて色々足りないことがあったので、記録を残しておくことにしました。

### 依存ライブラリの切り替え

build.gradleからSpringFox関連のものをごっそり消して、`springdoc-openapi-ui`を足します。
ちなみに私はKotlinを使っているので`springdoc-openapi-kotlin`も足しました。

Before:

```groovy
    // SpringFox
    compile group: 'io.springfox', name: 'springfox-core', version: springFoxVersion
    compile group: 'io.springfox', name: 'springfox-swagger2', version: springFoxVersion
    compile group: 'io.springfox', name: 'springfox-swagger-ui', version: springFoxVersion
    compile group: 'io.springfox', name: 'springfox-bean-validators', version: springFoxVersion
```

After:

```groovy
    // OpenAPI
    compile group: 'org.springdoc', name: 'springdoc-openapi-ui', version: springdocOpenApiVersion
    compile group: 'org.springdoc', name: 'springdoc-openapi-kotlin', version: springdocOpenApiVersion
```

### SpringのConfiguration

SpringのConfigurationクラスも差し替えます（クラス名は適当です）。
公式ドキュメントには単純な例しかなかったので、認証の設定がよくわからなくて苦戦しました。

Before:

```kotlin:SwaggerConfig.kt
@Configuration
@EnableSwagger2
class SwaggerConfig {

    @Bean
    fun swaggerSpringMvcPlugin(): Docket {
        return Docket(DocumentationType.SWAGGER_2)
            .select()
            .paths(paths())
            .build()
            .apiInfo(apiInfo())
            .securitySchemes(listOf(apiKey()))
            .securityContexts(listOf(securityContext()))
    }

    fun apiInfo() = ApiInfoBuilder()
        .title("my-awesome-api")
        .version("0.0.1")
        .build()

    fun apiKey() = ApiKey(
            "JWT",
            HttpHeaders.AUTHORIZATION,
            In.HEADER.name
        )

    private fun securityContext(): SecurityContext {
        return SecurityContext.builder()
            .securityReferences(defaultAuth())
            .forPaths(
                Predicates.or(
                    PathSelectors.ant("/api/v1/me/**")
                )
            )
            .build()
    }

    fun defaultAuth(): List<SecurityReference> {
        val authorizationScope = AuthorizationScope("global", "accessEverything")
        val authorizationScopes = arrayOfNulls<AuthorizationScope>(1)
        authorizationScopes[0] = authorizationScope
        return listOf(
            SecurityReference("JWT", authorizationScopes))
    }

    @Suppress("UNCHECKED_CAST")
    fun paths() =
        Predicates.containsPattern("/api/v1/*") as Predicate<String>
}
```

After:

```kotlin:OpenAPIConfig.kt
@Configuration
class OpenAPIConfig {
    @Bean
    fun openAPI() = OpenAPI()
        .info(
            Info().title("my awesome API")
                .version("0.0.1")
        )
        .components(
            Components()
                .addSecuritySchemes(
                    "bearer-key",
                    SecurityScheme()
                        .type(SecurityScheme.Type.HTTP)
                        .scheme("bearer")
                        .bearerFormat("JWT")
                )
        )
}
```

めちゃくちゃ短くなりました。

もっと機能を使い込んでいくと増えていくのかもしれませんが、とりあえずはこんなもんで足りてます。

ちなみに認証の設定はSecuritySchemeを定義するだけで、適用するURLとかの設定はここにはありません。
適用対象はコントローラやメソッドにアノテーションで指定するようです。

一部の設定がapplication.yml(application.properties)に移行しています。
色々指定できるっぽいので[Configuration properties](https://springdoc.org/springdoc-properties.html)に目を通しておくとよいでしょう。

```yaml:application.yml
springdoc:
  packagesToScan: my.awesome.api
  pathsToMatch: "/api/v1/**"
```

### コントローラ

使用するアノテーションが変わります。

この辺はspringdoc-openapiではなく[swagger-api/swagger-core](https://github.com/swagger-api/swagger-core)の方の話なので、そっちのドキュメントやソースを見たほうがよいでしょう。実際、ソース見たり色々指定しながら試しました。

以下、主に使っていたアノテーションとプロパティの置き換えメモ。
springdoc-openapiに置き換えたから明示的な指定が不要になったのか、実はSpringFoxでも明示的な指定が不要だったのか、今となっては定かではないプロパティもあります。

- `@ApiOperation` -> `@Operation`
    - `value` -> `summary`
    - `nickname` -> `operationId`（指定しなければメソッド名になります）
- `@ApiParam` -> `@Parameter`
    - `value` -> `description`
    - `required`とかつけなくても勝手にnullableかどうか見てやってくれました（Kotlinサポートのおかげかも？）。
    - `schema`を使えばなんでも表現できそうでした（特に`@Schema(implementation = Model::class)`が強そう）。
- セキュリティ設定 -> `@SecurityRequirement`
    - `@Operationのsecurity`でも個別に指定できます

Before:

```kotlin
@RestController
@RequestMapping("/api/v1/me")
class MeController {
    @ApiOperation(value = "アイテム取得", nickname = "getItem")
    @GetMapping("/items/{itemId}")
    @ResponseStatus(HttpStatus.OK)
    fun getItem(
        @ApiParam("アイテムID", required = true, example = "100")
        @PathVariable
        itemId: Int
    ): Item {
        return Item(itemId)
    }
}
```

After:

```kotlin
@SecurityRequirement(name = "bearer-key")
@RestController
@RequestMapping("/api/v1/me")
class MeController {
    @Operation(summary = "アイテム取得")
    @GetMapping("/items/{itemId}")
    @ResponseStatus(HttpStatus.OK)
    fun getItem(
        @Parameter(description = "アイテムID", example = "100")
        @PathVariable
        itemId: Int
    ): Item {
        return Item(itemId)
    }
}
```

### モデル

- `@ApiModel` -> `@Schema`
    - `title`はSwagger UIとかで表示名になります（デフォルトはクラス名）。
        - 日本語名とかつけるとデバッグ時とかにどのクラスなのかわかりづらくなるので、`title`はクラス名のままにして、日本語名は`description`に設定しました。
- `@ApiModelProperty` -> `@Schema`
    - こっちも`@Schema`を使うようです。
    - `allowableVariables`が`String`から`String[]`になりました。
        - アノテーションの引数にはコンパイル時定数しか指定できないので、やや困るケースもあります。
    - Enumの場合は勝手に各項目が列挙されます。
        - Enumにコード値とか持たせて、その値を使いたいときはまた色々工夫が必要です。
            - `toString`をオーバーライドするとか、Jacksonの`@JsonValue`、`@JsonCreator`を指定するとか。

Before:

```kotlin
@ApiModel(description = "アイテム")
data class Item(
    @ApiModelProperty("アイテムID", required = true, example = "100")
    val itemId: Int,

    @ApiModelProperty("アイテム名", required = true, example = "激ウマビスケット")
    val itemName: String,

    @ApiModelProperty("アイテム種別", required = true, allowableVariables = "CAKE,BISCUIT,JUICE", example = "BISCUIT")
    val itemType: String
)
```

After:

```kotlin
@Schema(description = "アイテム")
data class Item(
    @Schema(description = "アイテムID", example = "100")
    val itemId: Int,

    @Schema(description = "アイテム名", example = "激ウマビスケット")
    val itemName: String,

    @Schema(description = "アイテム種別", allowableVariables = ["CAKE", "BISCUIT", "JUICE"], example = "BISCUIT")
    val itemType: String
)
```

#### oneOfとか

SpringFoxではできませんでしたが、springdoc-openapiでは`oneOf`, `anyOf`, `allOf`, `not`なども表現できます。

これを使うと成功と失敗で返すモデルを変えるみたいな表現もできます。

```kotlin
@Schema(oneOf = [SuccessResponse::class, FailureResponse::class])
interface ApiResponse {
    @Schema(allowableVariables = ["success", "failure"])
    val status: String
}

data class SuccessResponse(
    override val status: String,
    val items: List<Item>
) : ApiResponse

data class FailureResponse(
    override val status: String,
    val error: String
) : ApiResponse
```

## おわりに

ドキュメントが薄っぺらいのでソースを読んだり、swagger-coreのほうを見に行ったりしないといけないのがやや辛かったです。
というか、挙動の大半はswagger-coreのほうだったりします。

いまいちな部分もなくはないですが、全体的にSpringFoxより良い感じがしますし、何より停滞気味のSpringFoxよりも今後に期待できそうなので、これから使うならspringdoc-openapiでよい気がします。