SwaggerはJSON API用のドキュメンテーション作成ツールです。
こんな画面が生成されて、画面の下の方にある"Try it out"というボタンで実際にリクエストを送信することができます。
SwaggerとPlay Framework
上の画面はSwagger UIというHTML+JavaScriptのツールによるもので、Swaggerの実体はコードアノテーションを解釈してエンドポイントに関するJSONを返してくれるツールです。
SwaggerのメインリポジトリであるSwagger CoreはScalaで書かれていて、Play Frameworkの用ライブラリもSwagger Coreのリポジトリ内にあります。
Play FramworkでSwaggerを使うサンプルアプリケーションも同じリポジトリにあります。
Swagger CoreはJAX-RS, Servlets, Play Frameworkに対応しているとありますが、仕様はかなり詳しく書かれていて、Scala以外の言語でも使うことができます。言語ごとのサポートツール一覧はこちらにあります。
Play Framworkだとこんなアノテーションになります。
Play Frameworkでの使い方
Play2用のライブラリのソースコードはこちらで、build.sbtにはこのように書くと使うことが使えます。
libraryDependencies ++= Seq(
"com.wordnik" %% "swagger-play2" % "1.3.12"
)
conf/routesにはこのように書きます。
GET /api-docs controllers.ApiHelpController.getResources
GET /api-docs/pet controllers.ApiHelpController.getResource(path = "/pet")
GET /api-docs/store controllers.ApiHelpController.getResource(path = "/store")
この例では、このアプリケーションのAPI群一覧やそのメタ情報を返すのが /api-docs で、 /api/pet/create や /api/pet/update など /pet/ 以下のパスに関するドキュメントを返すのが /api-docs/pet です。
アノテーションは主にControllerに対してつけます(が、Modelにつけるためのアノテーションもあります)。
Swagger自体の仕様はドキュメントが詳しいのですが、アノテーションにはドキュメントが無いっぽいのでコードを見て察する必要があります。
Swagger UIは静的ファイルだけなのでSwaggerと同じアプリケーション内に含む必要はありませんが、クロスオリジンの場合はCORSの設定が必要になります(どこで読んだか忘れました)。
同じアプリケーションに含める場合は、Playのpublicというフォルダ内にこのディレクトリを一式置いておけばいいです。
ハマるところ
Swagger UIからリクエストするURLがおかしい
Swaggerが出すJSONでbasePathというのがあり、Swagger Play2 Moduleではこのデフォルトがhttp://localhost
になっています。
このためSwagger UIでは
http://localhost/pets
のようなURLにアクセスしようとしてしまって、ポート9000で開発しているときなどに不便です。
これを変更するにはPlayのconf/application.confで
swagger.api.basepath="http://localhost:9000"
と書く必要があります。
その他認証周りのことなども書く必要があるみたいです。
Swagger UIのデフォルトのAPIエンドポイントにデモ用のものが入っている
このままだと使いにくいのでswagger-uiのindex.htmlを変更するか、
?url=http://localhost:9000/dev/api-docs
のようなクエリパラメータをつけてアクセスするとJSでよしなにやってくれます。
GETパラメータやパスに含むパラメータ
Swagger UI上でパラメータを弄ってリクエストできますが、デフォルトのbodyに含める方法ではなくGETパラメータを使う方法が最初わかりませんでした。
というのも、サンプルコードではこのように引数につけるアノテーションとしてApiParamというものがあるのですが、Swaggerの仕様ではPossible values are "query", "header", "path", "formData" or "body".
となっているものの、変えられるようなオプションが見当たらないためです。
def getPetById(
@ApiParam(value = "ID of the pet to fetch") @PathParam("id") id: String) = Action {
(追記: ↑よく見ると@PathParam
と書いてありますね。見落としてました。@QueryParam
というのもあるらしいです。下で紹介する方法はアノテーションの実装コードが完全に分離されるので優れていると思います)
コードを読んでるとApiImplicitParamsという似たようなものを見つけてそれでできることがわかりました。ApiParamはJAX-RSのためにあるものだそうです。
@ApiOperation(
nickname = "userPets",
value = "ユーザーのペット一覧",
response = classOf[Pet],
httpMethod = "GET"
)
@ApiImplicitParams(Array(
new ApiImplicitParam(name = "id", value = "ユーザーID", paramType = "path", required = true),
new ApiImplicitParam(name = "page", value = "ページ番号", paramType = "query", required = false)
))
def userPets(id: Long, page: Int) = Action {
(さっきREADMEにもこれの解説があることに気づきましたが、ApiImplicitParamとApiParamの説明が逆だと思います)
レスポンスがクラスと一対一対応ではないとき
response = classOf[Pet]
のような定義をするとこんな感じでレスポンスのデータ構造が見えたりするのですが、
僕の場合はこうやってdata
というキーをトップレベルに付けるようなヘルパーを作っていたので、
JsObject(Seq("data" -> Json.toJson(data)))
苦肉の策でアノテーションだけのためにclassを用意しました。
case class PetsJsonApiData(works: Seq[Pet])
@ApiOperation(
response = classOf[PetsJsonApiData]
)
まあSwagger UIの"Try it Out!"だと実際きちんとしたレスポンスが返ってきて表示されるので、自己満足的な面が強いです。(Swagger Toolでクライアントライブラリの自動生成とかをする場合などはこのようなことが重要になってきます)
Option
先ほどのデータ構造が見られる部分で、Stringだと""になってIntやLongだと0になるのですが、Optionを使ったり複雑なことをすると"Object"になるみたいでした。
クライアントライブラリの自動生成とかをする場合などは(以下略
僕はそこまで気にしないので放置しました。
使ってみた感想
- この手のツールの中では古くからあって枯れてるが、けっこう洗練されててやれることも多くて良い
- Swagger UIは綺麗でよく出来てる
- 最初にコード例見てアノテーションつらそうと思ったけどIDEの補完とかあるので気にならない
- すべて細かくアノテーションつける必要はなく、デフォルトのままのものとかコメントとかを省けばだいぶ気分的にもボリューム的にも軽い
- でも実装とコードが混ざってごちゃごちゃになるので、traitを作ってそっちにアノテーションを書くことにして、継承したControllerでoverrideすることにした
- API Blueprintと両方使ってみて
- ドキュメンテーション用ツールとしてはSwaggerのほうがメンテしやすそう
- ただしアノテーションの無い言語とかIDEがサポートしてない環境で使いたくはないかも
- API BlueprintはJSONを書くだけなのでプロトタイピング用ツールとしては優秀だった