SpringFoxに不満があったので、springdoc-openapiに移行してみたお話。
なにこれ
そもそも、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というページがあるので、そのとおりにやるだけ……なんですが、ドキュメントが簡素すぎて色々足りないことがあったので、記録を残しておくことにしました。
依存ライブラリの切り替え
build.gradleからSpringFox関連のものをごっそり消して、springdoc-openapi-ui
を足します。
ちなみに私はKotlinを使っているのでspringdoc-openapi-kotlin
も足しました。
Before:
// 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:
// 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:
@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:
@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に目を通しておくとよいでしょう。
springdoc:
packagesToScan: my.awesome.api
pathsToMatch: "/api/v1/**"
コントローラ
使用するアノテーションが変わります。
この辺はspringdoc-openapiではなく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:
@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:
@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
を指定するとか。
-
- Enumにコード値とか持たせて、その値を使いたいときはまた色々工夫が必要です。
- こっちも
Before:
@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:
@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
なども表現できます。
これを使うと成功と失敗で返すモデルを変えるみたいな表現もできます。
@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でよい気がします。