Play FrameworkでSwaggerを使う


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を書くだけなのでプロトタイピング用ツールとしては優秀だった