スキーマファースト開発のススメ
前後リンク
- RESTful API のおさらい
- Rails での JSON API 実装まとめ
- スキーマファースト開発
- The NEXT of REST
スキーマファースト開発
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 を選んだのかというと、
- JSON Hyper-Schema は Hypermedia の技術なので、1 サーバ 1 クライアント、同一チームで両方を見るという private API では出番が無い。
- RAML はコミュニティ規模が OpenAPI, API Blueprint に比べて小さかった
- OpenAPI と API Blueprint、生 JSON Schema だと、OpenAPI が一番「RESTful API」に特化していて、かつ詳細度が高い
といった辺りです。
OpenAPI は ruby だとライブラリが (当時は) 少なかったのですが、まぁ作れば何とかなるだろうと採用しました。
最近のトレンドでも Swagger 1 強になってるっぽくて、良い選択をしたなぁと思っています。
OpenAPI Initiative (OAI) と Swagger
OpenAPI Initiative という、REST API 記述方式の標準化を推進するための団体があり、 ここには Linux Foundation や Google, IBM, Microsoft 等の色んな企業が参加しています。
OAI が策定している記述方式が OpenAPI Specification です。
で、この OpenAPI Specification は、もともと Swagger が策定していた API 記述方式を OpenAPI Initiative に寄贈したものです。(ツールが先にあって、後から標準化を進めている)
仕様を提供しただけで、周辺ツールは依然として「swagger」です。
スキーマファースト開発のためのツールたち
スキーマが存在することによって、
- ドキュメント自動生成
- クライアント自動生成
- request validator 自動生成
- response spec 自動生成
ができるようになります。
この導入が数手で終わる状態で、「2017 年の Web API 開発の一般的な構成」に沿った状態が作れたと言えて、 その上に自分たちの制約を足していくのが良いのだと思う。
ドキュメント自動生成
YAML をもとに見やすい HTML を生成するのは swagger-ui で行っています。
https://swagger.io/swagger-ui/
http://petstore.swagger.io/ (demo)
- どんなパラメータで呼び出すとどういうレスポンスが返るのか
- 呼び出すときの認証方式
- deprecated な API
等の API Spec を綺麗に表示してくれる他、ドキュメント上から直接 API call もできます。
ドキュメント生成支援
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 の書き味が悪い
- スキーマと Serializer とで同じことを書いていないか?
- スキーマと Routes とで同じことを書いていないか?
- エラーやページネート、バージョニングどうしよう?
最後のはあまり YAML 関係なく、API 開発全体のツラミですね。
細かく書くとそれぞれ 1 記事書けるので、上の 2 つだけ紹介します。
YAML の書き味
OpenAPI は RESTful な API を書くのに向いているだけあって、リソースを中心に Request, Response パラメータを書いていけるのが良いです。
ただ、YAML の書き味はデフォルトだとそんなに良くない。
具体的には以下に手を入れました。
- デフォルトで required にする
- 別ファイルを参照できる仕組みを入れる
- 別ファイルとマージできる仕組みを入れる
デフォルトで 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 Reference と JSON Pointer という仕様があります。
JSON Reference が別ファイルを参照する仕組みで、JSON Pointer は XPath みたいなヤツです。
これの YAML 版を作りました。
user:
$ref: "user.yml#/"
で user.yml のルート要素が展開されます。
これを全力で使いたいので、ディレクトリ構造を
- path
- definition
に分け、すべて別ファイルへの参照として表現するようにしました。
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 を受けた後のオブジェクトへのマッピングも自動生成できます。
スキーマファースト開発のデメリット
「スキーマを書くのが手間」の一点に尽きるんだけれど、 ここまで見てきたように、極力手間をなくすように改善しています。 また、型の恩恵というメリットがデメリットにも殴り勝てるぐらいの大きさなんじゃないかと思う。
前後リンク
- RESTful API のおさらい
- Rails での JSON API 実装まとめ
- スキーマファースト開発
- The NEXT of REST