GeekFactory

int128.hatenablog.com

Swagger YAMLのバリデーションをCIで回す

Swagger YAMLを書いていると間違いに気づかないことがよくあります。リポジトリにpushした時にJenkinsやCircle CIなどで構文チェックできるとミスを防げるます。そこで、GradleのプラグインにSwagger YAMLのバリデーションを追加してみました。

使い方

build.gradleに下記を書きます。

plugins {
  id 'org.hidetake.swagger.generator' version '1.6.0'
}

validateSwagger {
  inputFile = file('petstore.yaml')
}

下記のようにvalidateSwaggerタスクを実行すると、YAMLのバリデーションが実行されます。

% ./gradlew validateSwagger
:validateSwagger

YAMLに間違いがあると、下記のようなエラーを表示してくれます。

:validateSwagger FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':validateSwagger'.
> Invalid Swagger YAML: /Users/hidetake/repo/gradle-swagger-generator-plugin/acceptance-test/validator/petstore.yaml
  ---
  - level: "error"
    schema:
      loadingURI: "http://swagger.io/v2/schema.json#"
      pointer: "/definitions/parametersList/items"
    instance:
      pointer: "/paths/~1pets~1{petId}/get/parameters/0"
    domain: "validation"
    keyword: "oneOf"
    message: "instance failed to match exactly one schema (matched 0 out of 2)"
    matched: 0
    nrSchemas: 2
(以下略)

仕組み

Swaggerで公式に用意されているJSONスキーマYAMLを検証しています。

https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v2.0/schema.json

Gradleプラグインについて

Gradle Swagger Generator Pluginはバリデーションだけでなくコード生成やドキュメント生成の機能も提供しています。詳しい使い方はREADMEを参照してください。

github.com

Spring Bootで例外発生時にJSONを返す

Spring BootのREST APIサーバで例外発生時のエラー情報をJSONで返す方法を調べたのでメモです。

やりたいこと

  • 例外が発生した場合は常にエラー情報をJSONで返したい。
  • 例外の種類によってステータスコードを分けたい。例えば、バリデーションエラーが発生した場合は400、その他は500を返す。
  • Spring MVCの本来の振る舞いは変更しない。例えば、認可エラーで401を返す振る舞いは維持する。

実現方法

  • ErrorController インタフェースを実装すると、例外時のレスポンスをカスタマイズできる。
  • キャッチした例外によってステータスコードを変更する。

ステータスコードを指定できる例外クラスを作成します。実際のプロダクトでは、エラーコードやエラーメッセージを含む業務例外クラスを作成するとよいでしょう。

class AppException extends RuntimeException {
    final int status

    def AppException(int status, String message) {
        super(message)
        this.status = status
    }

    def AppException(int status, String message, Throwable cause) {
        super(message, cause)
        this.status = status
    }
}

ErrorController インタフェースの実装クラスを作成します。 ErrorAttributes#getErrorAttributes() メソッドでエラー情報のMapを取得できます。

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.web.ErrorAttributes
import org.springframework.boot.autoconfigure.web.ErrorController
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.context.request.ServletRequestAttributes

import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@RestController
@RequestMapping(produces = 'application/json')
class AppErrorController implements ErrorController {
    @Autowired
    ErrorAttributes errorAttributes

    @Value('${debug:false}')
    boolean debug

    @RequestMapping('/error')
    Map<String, Object> error(HttpServletRequest request, HttpServletResponse response) {
        // エラー情報を取得する
        def servletRequestAttributes = new ServletRequestAttributes(request)
        def attributes = errorAttributes.getErrorAttributes(servletRequestAttributes, debug)
        def cause = errorAttributes.getError(servletRequestAttributes)
        if (cause instanceof AppException) {
            // 例外に含まれるステータスコードとメッセージを返す
            response.status = cause.status
            attributes.status = cause.status
            attributes.error = HttpStatus.valueOf(cause.status).reasonPhrase
            attributes.message = cause.message
        }
        attributes
    }

    @Override
    String getErrorPath() {
        '/error'
    }
}

例えば、ステータスコード400を返す場合は下記のような例外を投げます。

    @GetMapping('/hello')
    void hello() {
        throw new AppException(400, 'Custom Validation Error')
    }

レスポンスの例

ステータスコードを指定した例外(上記のAppException)を投げた場合は下記になります。この場合はログにスタックトレースが出力されます。

{
  "timestamp":1480254775918,
  "status":400,
  "error":"Bad Request",
  "exception":"example.server.AppException",
  "message":"Intentionally 400 raised",
  "trace":"example.server.AppException: ...(debugが有効の場合のみスタックトレースが付く)",
  "path":"/hello"
}

401の場合:

{
  "timestamp":1480254600193,
  "status":401,
  "error":"Unauthorized",
  "message":"Bad credentials",
  "path":"/hello"
}

404の場合:

{
  "timestamp":1480255037435,
  "status":404,
  "error":"Not Found",
  "message":"No message available",
  "path":"/hello"
}

REST APIクライアントの考慮

以下のように例外処理を設計するとよいでしょう。

  • ステータス200でJSONが返ってきた場合、正常
  • ステータス200だがJSONを解釈できない場合、共通エラー
  • ステータス200以外でJSONが返ってきた場合、エラーコードに応じた例外処理
  • ステータス200以外でJSONを解釈できない場合、共通エラー
  • 接続失敗の場合、共通エラー

参考文献です。

Swagger Codegenにおけるspring-cloudテンプレートのカスタマイズ

Swagger Codegenではコード生成のテンプレートをカスタマイズできますが、ライブラリに spring-cloud を選んだ場合はカスタマイズの仕方に注意が必要です。

springのテンプレート構造は以下になります。

  • template/
    • api.mustache ←すべてのライブラリで共通のテンプレート
    • ...
    • libraries/
      • spring-mvc/
      • spring-boot/
      • spring-cloud/
        • apiClient.mustache ←spring-cloud固有のテンプレート
        • ...

spring-cloud直下のapiClient.mustacheを変更しても、なぜかコードに反映されません。そこで、libraries直下にapiClient.mustacheをコピーして変更すると、コードに反映されるようになりました。

  • template/
    • api.mustache ←すべてのライブラリで共通のテンプレート
    • apiClient.mustache ←ここに配置するとカスタマイズが効く
    • libraries/
      • ...
      • spring-mvc/
      • spring-boot/
      • spring-cloud/
        • apiClient.mustache ←ここを編集してもカスタマイズが効かない
        • ...

罠にはまったので参考になれば幸いです。