スキーマファースト開発のススメ

前後リンク

スキーマファースト開発

API 記述言語以前の世界:

API 記述言語以前の世界

サーバ側を実装してからクライアント側を実装。結合すると不具合があるのでそれぞれ直して、テストしてリリース。

API 記述言語以後の世界:

API 記述言語以降の世界

まず一緒に相談しながらスキーマを定義して、スキーマをもとにパラレルに実装。 結合してもほとんど不具合は見当たらないのでそのままリリース。

となり、認識違いや結合時のトラブルが少なくなるので加速が期待できます。

ドリコムで選択した API 技術

ドリコムでは、API 技術の採用にこのような歴史を歩んできました。

app initial commit api tech
A 2013 jbuilder
B 2013 jbuilder
C 2013 jbuilder
D 2013 jbuilder + AMoS
E 2014 jbuilder
F 2014 AMoS
G 2015 jbuilder
H 2016 AMoS + JSON Schema
I 2016 AMoS + JSON Schema
J 2016 AMoS + JSON Schema
K 2016 OpenAPI
L 2017 OpenAPI
M 2017 OpenAPI

第 2 部

現在 5 派閥ぐらいありそうです。

と書いた中でなぜ OpenAPI を選んだのかというと、

といった辺りです。

OpenAPI は ruby だとライブラリが (当時は) 少なかったのですが、まぁ作れば何とかなるだろうと採用しました。

最近のトレンドでも Swagger 1 強になってるっぽくて、良い選択をしたなぁと思っています。

Google Trend

OpenAPI Initiative (OAI) と Swagger

OpenAPI Initiative という、REST API 記述方式の標準化を推進するための団体があり、 ここには Linux Foundation や Google, IBM, Microsoft 等の色んな企業が参加しています。

OAI が策定している記述方式が OpenAPI Specification です。

で、この OpenAPI Specification は、もともと Swagger が策定していた API 記述方式を OpenAPI Initiative に寄贈したものです。(ツールが先にあって、後から標準化を進めている)

仕様を提供しただけで、周辺ツールは依然として「swagger」です。

スキーマファースト開発のためのツールたち

スキーマが存在することによって、

ができるようになります。

この導入が数手で終わる状態で、「2017 年の Web API 開発の一般的な構成」に沿った状態が作れたと言えて、 その上に自分たちの制約を足していくのが良いのだと思う。

ドキュメント自動生成

YAML をもとに見やすい HTML を生成するのは swagger-ui で行っています。

https://swagger.io/swagger-ui/

Swagger UI

http://petstore.swagger.io/ (demo)

等の API Spec を綺麗に表示してくれる他、ドキュメント上から直接 API call もできます。

ドキュメント生成支援

Swagger Editor で行っています。

http://editor.swagger.io/

Swagger Editor

Syntax Highlight や入力補完、リアルタイムバリデーション等を備えています。

この辺りは docker で立ち上げて使うのが楽で良いと思います。

クライアント自動生成

Swagger Codegen https://swagger.io/swagger-codegen/ から Ruby でも PHP でも Java でも Swift でも何でも吐き出せます。

テンプレートは mustache で書かれていてカスタマイズ可能。

Request Validator / Response Validator

Rack 層でチェックできます。具体的には committee ですね。 committee は 2017-09-06 にリリースされた v2.0.0 で OpenAPI Spec も使えるようになったので、 更に便利になりました。

(それまでは OpenAPI Spec から JSON Schema に変換していた)

毎日スキーマの YAML を書く中で出会ったツラミ

スキーマの YAML を書いていく毎日なんですが、様々な問題に直面します。

最後のはあまり YAML 関係なく、API 開発全体のツラミですね。

細かく書くとそれぞれ 1 記事書けるので、上の 2 つだけ紹介します。

YAML の書き味

OpenAPI は RESTful な API を書くのに向いているだけあって、リソースを中心に Request, Response パラメータを書いていけるのが良いです。

ただ、YAML の書き味はデフォルトだとそんなに良くない。

具体的には以下に手を入れました。

デフォルトで required

OpenAPI (v2) では required を以下のように書くんですが、

type: object
required:
  - name
  - age
properties:
  name:
    type: string
  address:
    $ref: "#/components/schemas/Address"
  age:
    type: integer
    format: int32
    minimum: 0

ツラくないですか?

僕は各プロパティに書きたい。

 type: object
-required:
-  - name
-  - age
 properties:
   name:
     type: string
+    required: true
   address:
     $ref: "#/components/schemas/Address"
   age:
     type: integer
     format: int32
     minimum: 0
+    required: true

もしくは、更に一歩進めてデフォルトを required にして漏れないように。

 type: object
-required:
-  - name
-  - age
 properties:
   name:
     type: string
   address:
     $ref: "#/components/schemas/Address"
+    optional: true
   age:
     type: integer
     format: int32
     minimum: 0

optional という独自拡張ですが、Swagger に食わせる前に optional -> required への変換を行うことが可能で、変換後は OpenAPI Spec に沿った YAML になります。

このように、共通認識を作ったり、巨人の肩に乗ったりするためには、 オープンな仕様に基づいていることがとても大事です。 周辺ツールのエコシステムを利用できなくなると、Swagger を選ぶ理由も無くなっちゃいます。

別ファイルを参照できる仕組みを入れる

JSON には JSON ReferenceJSON Pointer という仕様があります。

JSON Reference が別ファイルを参照する仕組みで、JSON Pointer は XPath みたいなヤツです。

これの YAML 版を作りました。

user:
  $ref: "user.yml#/"

で user.yml のルート要素が展開されます。

これを全力で使いたいので、ディレクトリ構造を

に分け、すべて別ファイルへの参照として表現するようにしました。

api
|-- definitions
|   |-- index.yml
|   `-- user.yml
|-- paths
|   |-- index.yml
|   `-- users
|       |-- create.yml
|       `-- index.yml
`-- schema.yml
# schema.yml
paths:
  $ref: paths/index.yml#/
definitions:
  $ref: definitions/index.yml#/
# paths/index.yml
/users:
  get:
    $ref: users/index.yml#/
  post:
    $ref: users/create.yml#/

別ファイルとマージできる仕組みを入れる

$ref は (JSON Reference では) そのオブジェクトの完全置き換えを示すので、 以下のような場合に対応するために $merge を導入しました。

# user.yml
properties:
  name:
    type: string
# user_detail.yml
properties:
  $merge: "user.yml#/"
  description:
    type: string

単純な置き換えではなく、deep merge します。

# user_detail.yml
properties:
  user:
    $ref: "user.yml#/"
  description:
    type: string

とキーを工夫することで $ref でも実現できるんですが、merge ができると色々効いてきます。

こちらも $merge のみ解決した場合は valid な OpenAPI Spec に従っているので オープンな仕様に乗っ取ったまま書き味を良くすることに成功しました。

スキーマと Serializer とで同じことを書いていないか?

スキーマには以下を書きます。

properties:
  name:
    type: string
  birthday:
    type: date-time
  is_admin:
    type: boolean

Serializer には以下を書きます。

class UserSerializer < AMo::Serializer
  attributes :name,
             :birthday,
             :is_admin

  def birthday
    object.birthday.strftime("%Y-%m-%d")
  end

  def is_admin
    object.admin?
  end
end

かなりの部分が重複してそうですね。

そこで、スキーマをもとに Serializer を動的に生成するようにしました。 具体的には public_send と、型変換の自動適用です。

def serialize(schema)
  schema.properties.map {|key, schema|
    if object.respond_to?(key)
      value = object.public_send(key)
      coerce_recursive(value, schema) # schema に合わせて型変換
    else
      # foo? と is_foo の変換とか
    end
  }
end

def coerce_recursive(value, schema)
  case [schema.type, value.class]
  when ["date-time", Time]
    value.strftime("%Y-%m-%d")
  when ...
  when ...
  end
end

みたいな。(概念コード)

オブジェクト自身が「どうなればスキーマに合うようにシリアライズできるか」を知っているので、 アプリケーションの記述は ActiveModelSerializer を使っていたときと変わらず以下のみです。

def show
  render json: @user
end

まとめ

信頼できるドキュメント

Serializer をスキーマから自動生成することで、「スキーマを書かないと Serialize できない」となりました。(やろうと思えば to_json とかで書けますが、レビューで撥ねます)

これによって「100% 信頼できる API ドキュメント」というものが生まれます。

Request / Response に型を持ち込む

スキーマに型が書いてあり、確実にそれが守られているので HTTP / JSON 上に型が生まれます。

例えばクエリパラメータに含まれるパラメータは文字列なので、しばしば

params[:user_id].to_i

params[:force] == "true"

といったコードを書いたことがあるかと思います。

これをスキーマに沿って変換することで、アプリケーションが取り出したときには それぞれ Integer, Boolean になっています。

インタフェースがサーバ/クライアント双方のものになる

YAML なので Ruby のコードと違い、それほど嫌悪感を持たずに読めます。

クライアントプログラマとサーバプログラマの間に言語の壁があるときに、 精神的な壁を取り払うことができるのはとても大きいです。

また、YAML をもとに mock サーバも立ち上がるので、普段は mock サーバで開発し、 開発が終わった段階で結合することができるようになります。

「ActiveModelSerializer を読んでくれ」だと、Ruby っぽさが前面に出すぎていて サーバサイドのものという意識になりますが、共通で使う YAML なら双方のものになります。

クライアントの開発も楽に

mock サーバにより開発に使えるレスポンスがあるのもそうですが、 「どんなリクエストを受け付けるか」をスキーマに書いてあるので Request を投げる部分のコードを自動生成できます。

また、「そのリクエストのレスポンスが何か」もスキーマに書いてあるので Response を受けた後のオブジェクトへのマッピングも自動生成できます。

スキーマファースト開発のデメリット

「スキーマを書くのが手間」の一点に尽きるんだけれど、 ここまで見てきたように、極力手間をなくすように改善しています。 また、型の恩恵というメリットがデメリットにも殴り勝てるぐらいの大きさなんじゃないかと思う。

前後リンク