開発

スキーマ駆動 WebAPI開発「スキーマ駆動開発とは何か」「OpenAPIの基本」

WEBサービスを運用していて、そのサービスはいろんな機能をもってる・そしてそんなサービスが複数ある。

そんな条件で運用している運用担当の方は多いのではないでしょうか?
そして同時に議題にあがるであろう仕様書が維持されない問題、かつ後から編集しようとした際に何がなんだか分からなくなる問題。

その折に「WEB+DB Press vol.108」にてスキーマ駆動開発なるものが紹介されていたので自身が嚙み砕く為に内容をまとめなおしてみました。

なにやら「スキーマ駆動開発」とは一定以上の規模の開発で、如何にいい感じにやるかを突き詰める為の開発のようです。

まず、「WEB+DB Press vol.108」のスキーマ駆動開発特集のうち「スキーマ駆動開発とは何か」「OpenAPIの基本」について記載してみました。

スキーマ駆動開発とは何か

スキーマ駆動開発とは、最初にWeb APIの開発者と利用者が一緒になってスキーマの設計を行う。
開発の中心にスキーマがある。

Web開発は開発チーム全体のワークフロー設計が肝であり、複数人で行う以上必ず「やりとり」が発生する。
このやりとりをスムーズに「これはいい感じじゃない?」というのがスキーマ駆動開発

Web APIとは

まず、Web APIとはHTTP(S)のインターフェイスで提供されるAPIのこと。
今、WebAPI開発を上手に進めるための方法論やツールが求められている。

Web APIの分類

よくあるWeb APIの大別方法として下記がある。

  • SSKDs (Small Set of Known Developers)
    特定少数の開発者たちのこと

  • LSUDs (Large Set of Unknown Developers)
    不特定多数の開発者たちのこと

Web API開発のがんばりどころ

開発チーム全体のワークフロー設計にあるのでは?
APIである以上、提供する側と利用する側の両端がある
発生するやりとりを含めた全体設計をいかにうまくやるかがプロジェクトを成功させる鍵であると思われる。

Web APIのスキーマとは

スキーマとはその対象の「ありさま」を定められた形式で記述したもの。
Web APIのスキーマをざっくりいうとどこにどんなリクエストを送ったらどんなレスポンスがかえってくるかを記述したもの。
このスキーマを中心に人が動いていくのが「スキーマ駆動Web API開発」。

スキーマ駆動のスタイルが登場する前の開発風景

  • 実装のみ
    実装のみで自動化されたソフトウェアテストやドキュメントは一切ない状態。

  • 実装とドキュメントがある
    人間が人間向けに書いたドキュメントがある状態。
    人間にとって読みやすい。

短所は「実装とドキュメントの乖離が生じ得る」こと。

  • 自動化されたソフトウエアテストがドキュメント代わり
    自動化されたソフトウエアテストのコードやテスト結果を人間向けに書かれるドキュメントの代わりに活用する方法。
    テストコードによってテストケースがきれいに構造化されており振る舞いに対し網羅的であればそれは人間向けに書かれていないとしてもドキュメントの役目を果たす。

長所は「実装と同期したドキュメント相当のものが得られる」。

短所は人間向けに書かれたドキュメントより読みにくいものになる。

スキーマのなかったこれまでの開発現場

プログラマーは可能な限り「確定したもの」に基づいてコードを書きたいと思うもの。

スキーマ駆動Web API開発

この場において「スキーマ駆動API開発」と呼ぶスタイルではWebAPIの開発者と利用者が一緒になってスキーマの設計を行う

例:Webアプリケーション開発者とモバイルアプリ開発者が相談しながらスキーマを決めていくような状況

開発効率の向上

  • マイクロサービスのように複数の開発者がそれぞれ別のサーバーを担当しており、サーバー間でWebAPIアクセスが必要なとき

スキーマを活用するとサーバーとクライアント開発の開発が平行して進む

スキーマ設計

実装

検証
※うまくいかなかったらスキーマ設計にもどる

完成

開発の中心にスキーマがある

スキーマ活用のシーン

サーバの実装

サーバ実装のテスト

自動化テストを定常的に実行するCI(Continnous Integration)環境を整備すればコード変更の際スキーマから外れる挙動になってしまってもすぐ気付くことができる。

ドキュメントの生成

スキーマはプログラムから処理しやすい形式で書かれるのでそれ自体は必ずしも人間にとって読みやすいものではない。
そこから人間向けのドキュメントを自動生成することができる。

スタブサーバの活用

スキーマを読み込んで動作するスタブサーバーがあればスキーマを用意できたと同時にクライアントの実装に活用できる。

OpenAPI

SwaggerからOpenAPIへ。
その変遷。

  • 2010年
    Tony Tam氏を中心にSwaggerプロジェクトがはじまる。

  • 2015年11月
    The Linux Foundationの協力のもと、Microsoft、Google、IBM、PayPal、SmartBearなどの企業により標準化団体としてOpenAPI Initiativeが結成される。

その後、Swagger SpecificationはOpen Apiに寄贈される。


– 2016年1月
OpenAPI Specification2.0にリネUI
企業やプロダクトの枠を超えて議論が続けられバージョンアップが続く。


– 2019年6月
最新バージョンは3.0.2

結果、現在においてSwaggerは様々なスキーマフォマットの中から最もシェアを獲得する。

勝因についてTony Tam氏が語るポイントは下記の3つ。

  • 1.ヒューマンリーダブルであること
  • 2.マシンリーダブルであること
  • 3.徹底していること

APIの仕様を記述するフォーマットであるSwagger SpecificationはOpenAPI Initiativeに寄贈されたが、Web APIの開発を効率化するツール群であるSwagger Toolは引き続きSwaggerプロジェクト配下で開発されている。

Swagger Toolのそれぞれ

Swagger UI

OpenAPIで記述されたスキーマをドキュメント化するツール。

エンドポイントやパラメーターなどを記述した内容が自動的に整形されて表示される。

Swagger Editor

OpenAPIのスキーマを記述することに特化したオンラインエディタ。

Swagger Codegen

OpenAPIのスキーマからソースコードを生成するコードジェネレーター。
APIクライアントやスキーマに定義されたサンプル値に基づいてレスポンスを返すスタブサーバーのソースコードを生成でき、さまざまなプログラム言語に対応している。

本来は「OpenAPI」というと何も差すわけではないが、一般的に「OpenAPI Specification」の意味で使われることが多い。

OpenAPIの基本

~REST APIの仕様をスキーマとして記述する~

サンプルスキーマ(匿名掲示板の場合をYAML{YAML Ain’t Markup Language}で記述)

openapi: 3.0.2
info:
title:匿名掲示板 API
version: 1.0.0
servers:
...
paths:
/posts:
get:
description:投稿をすべて取得する
operationId:getPosts
...
post:
description:投稿を作成する
operationId:createPosts
...
/posts/{postId}:
get:
description:投稿を取得する
operationId:getPost
put:
description:投稿を更新する
operationId:updatePost
delete:
description:投稿を削除する
operationId:deletePost
...
/posts/{postId}/comments:
get:
description:投稿に付いたコメントを全て取得する
operationId:getComments
...
post:
description:投稿にコメントを付ける
operationId:createComments
...
/posts/{postId}/comments/{commentId}:
delete:
description:投稿に付いたコメントを削除する
operationId:deleteComments
...
components:
schemas:
Post:
type: object
properties:
post:
$ref: '#/components/schemas/PostProperties'
required:
  -post
...
parameters:
postIdParam:
name: PostId
in: path
description:投稿のID
required: true
schema: 
type: integer
...
responses:
NotFound:
description:リソースが見つからない
content:
application/json:
schema:

OpenAPIオブジェクト

OpenAPIではスキーマの各要素を「オブジェクト」という単位で役割を決める。

上記サンプルスキーマの構造をオブジェクトの階層を当てはめてみる。

スキーマ全体を「OpenAPIオブジェクト」と呼ぶ。

openapi: 3.0.2 ← OpenAPIオブジェクトのopenapiフィールドの値は3.0.2という意味
info:  ← Infoフィールドの値としてInfoオブジェクトが存在 
  title:匿名掲示板 API
  version: 1.0.0
servers: ← 「Serversオブジェクト」APIをホストするサーバー情報を記述する

...
paths: ← 「Pathsオブジェクト」Web APIエンドポイントの仕様を記述する。
パス(/Postsなど)をキーにしたPath Itemオブジェクトを持ち、1つ以上のPath Itemオブジェクトで構成され、複数のOperationオブジェクトを内包している。

/posts:
...
/posts/{postId}:
...
/posts/{postId}/comments:
...
/posts/{postId}/comments/{commentId}:
...
components: ← スキーマ内の様々な場所で再利用するオブジェクトを定義しておく要素。
Componentsオブジェクトを使用せずにリクエストボディなどの仕様をPathオブジェクトに
インラインで記述することも可能。
ただ、スキーマ全体の見通しの良さや再利用性の観点から、
Componentsオブジェクトを積極的に使用することがオススメ。

schemas:
...
parameters:
...
responses:

Operationオブジェクト

paths:
/posts:
...
post:
description:投稿を作成する
operationId:createPosts ← Operationオブジェクトを識別する文字列。
スキーマの中で一意である必要がある。
このフィールドの値はコードジェネレーターでAPIクライアントを自動生成する際にメソッド名として利用される
requestBody:
$ref: '#/components/requestBodies/Post' ←Referenceオブジェクトを使用し、別の箇所に定義したRequestBodyオブジェクトを参照している。
response:
'201':
$ref: '#/components/responses/Post' ←Referenceオブジェクトにより、別の箇所に定義したReferenceオブジェクトを参照している。再利用することで定義を1ヶ所にまとめて重複を避けることができる。
$refという名前のフィールドをキーにするスタイルはJSON Referenceから採り入れられた構文。
...

ComponentsオブジェクトとSchemaオブジェクト

...
components: ← Componentsオブジェクト
schemas: ← Schemaオブジェクト。リクエストやレスポンスで扱うデータの型や構造を定義するオブジェクト。
Post:
  type: object ← typeフィールド。データの型を定義。
  properties: ← propertiesフィールド。そのオブジェクトが持つプロパティを定義する。このフィールドの値はReferenceオブジェクトまたはSchemaオブジェクトである必要がある。
  post:
  $ref: '#/components/schemas/PostProperties' ← postプロパティの値をReferenceオブジェクトで定義
  required:
  -post
...  
PostProperties:  
type: object
properties
  id:
  type: integer ← Schemaオブジェクト。idプロパティの値をSchemaオブジェクトで定義している。Schemaオブジェクトの定義にSchemaオブジェクトが使用されていることに注意。
  example: 1 ← exampleフィールド。そのプロパティが取り得る値を定義。この値はスキーマの仕様をより具体的に示すため、そしてコードレスジェネレーターを利用して生成するスタブサーバーがレスポンスを返す値としても使用され、スキーマ駆動開発を行う上では必須といえる。
...
  posted_at:
    typee: string
    format: date-time
    example: '2018-12-01T00:00:00Z'
  required: ← このSchemaオブジェクトの必須パラメーターを定義する。  
    -id
    -title
    -content
    -posted_at  

Schemaオブジェクトには他にも様々なフィールドがある。

例えば、下記がある。

order_number;
  schema:
    type: integer
    format: int64
    minimum: 1
    maximum: 50

minimumとmaximumでは数値の下限と上限を指定できる。

ParametersオブジェクトではAPIサーバーに送信するパラメーターを定義する。

Parameterオブジェクトはnameフィールドとinフィールドの組み合わせで一意になる。

...
parameters:
  postIdParam:
  name: PostId ← nemeフィールド。パラメーター名を定義する。
  in: path ← パラメーターの場所。query、header、path、cookieが指定可能。
  description:投稿のID
  required: true
  schema: 
  type: integer
...

Request Bodyオブジェクト、Responseオブジェクト

requestBodies: ← Request Bodyオブジェクト。APIサーバーへ送信するリクエストボディを定義する
Post:
description: 作成する投稿
required: true
content: ← Request Bodyオブジェクトのペイロードの構造を定義。この部分はペイロードの部分はMedia Type オブジェクトとして規定されている。 
application/json
  schema:
  $ref: '#/components/schemas/PostRequest'...Reference(Schema)
...
responses: ← Responsesオブジェクト。APIサーバから帰ってくるレスポンスを定義する。
  Post:
    description: 投稿
    content: ← Responsesオブジェクトのペイロードの構造を定義。
      application/json
        schema:
          $ref: '#/components/schemas/Post'...Reference(Schema)

パスの記法

Path Templatingのサンプル

定義したパラメーター名はコードジェネレーターでAPIクライアントを自動生成する際メソッドの引数名などに使用される。命名規則に気を使うとよい。

paths:
...
  /posts/{postId}: ← エンドポイントのパスにパラメーターが含まれる場合は
  波括弧({})を使用する。
  get:
    description: 投稿を表示する
    operationId: getPost
    parameters:
      -ref: '#/components/parameters/postIdParam'
      responses:
      ...      
components:
...
    parameters:
      postIdParam:
        name: PostId ← パスでパラメーターを使用する注意点としてparameterフィールド配下に必ず
        同名のParameterオブジェクトが存在する必要がある
        in: path
        description: 投稿のID
        required:true ← in: pathが指定されたParameterオブジェクトはr
        equired:trueが定義されている必要がある
        schema:
          type: integer

スキーム駆動開発を実践する際はOpenAPIの仕様を参照してできる限り詳細に記述するとよい。
たとえば、minimumとmaximumを使用しパラメーターの下限/上限を定義しておくことでAPIクライアントが定義内容に従ってパラメーターのバリデーションを行うようになる、

つまり、スキーマの精度を高めることがシステムの質を高めることにつながる

スキーマの管理

スキーマはプレーンなテキストファイル。
その為、ソースコードと同様にバージョン管理するとよい。

スキーマはAPIサーバーのレスポンステストでも利用する為、APIサーバーと同じリポジトリで管理するとよい。

長大なテキストファイルは見通しが悪くメンテナンスが困難になる。Referenceオブジェクトを利用してスキーマを分割するとよい。

Referenceオブジェクトは外部ファイルのオブジェクトも参照することができる。

スキーマを分割した際に問題になるのがSwagger Editorのバリデーション機能。
Swagger Editorはオンラインエディタの為、ローカルマシン上で分割したスキーマを読み込むことができずバリデーションエラーが出てしまう。

本章を記述された中野暁人 氏/アスクル(株)が勧めるのはVisual Studio CodeのSwagger Viewerという拡張機能。
ReferenceオブジェクトはJSON Referenceという仕様に倣っている。
JSON Referenceではオブジェクトが持つ値(参照先)はURI(RFC 3986)であると定義されている。
つまり、第三者が管理するドメイン配下のスキーマを参照することも可能なのである。
下記、サンプル。

schema:
  $ref: https://example.com/example.yeal

外部オブジェクトを参照する際の注意点

万が一参照先に悪意がある場合不正なスキーマを経由してコードジェネレーターが生成するソースコートに悪意のあるコードを埋め込まれてしまいコードインジェクション攻撃を受けてしまう可能性がある

OpenAPI 3.0では何が変わったのか?受け継がれるSwaggerが目指したゴール

まず、スキーマのトップレベルの構造が見直された。
スキーマのルートであるOpenAPIオブジェクトに直接属する要素が移動/統合されたことでよりシンプルな構造になった
従来の
– difinitions
– parameters
– responses
といった要素を再利用するためトップレベルの要素を新設したComponentsオブジェクト配下におき、更にComponentsオブジェクトを扱う要素を拡充。
スキーマを構成する要素の再利用可能性が向上した

仕様の記述においてデータ表現は重要な要素になる
OpenAPI 3.0で強化された表現方法の中にシリアライズ形式の拡充がある。

OpenAPI 2.0以前

  • 配列のパラメーターを扱う場合
    ParameterオブジェクトのcollectionFormatフィールドでcsvやssv(space separeted value)といったフォーマットを定義していた。

  • swagger2.yaml

parameters:
 SearchWords:
 name: search_words
 in: query
 type: array
 items:
 type: string
 collectionFormat: ssv

OpenAPI 3.0

OpenAPI3.0ではより多彩なデータを扱う為、collectionFormatを廃止。
代わりにstyleフィールドが追加された。
styleフィールドでは配列やオブジェクト型のデータのシリアライズ形式を定義する位置づけになっている。

  • openapi3.yaml
parameters:
 SearchWords:
 name: search_words
 in: query
 schema:
  type: array
  items:
    type string
  style: spaceDelimited   

openapi3.yamlは従来のCollectionFormat: ssvをOpen APIで記述したもの。
spaceDelimitedという名称どおり、スペース区切りのシリアライズ形式であることを定義している。

さらに、explodeフィールドと組み合わせることで多彩なシリアライズ形式を表現することができる。

parameters:
  City:
  name: city
  in: path
  schema:
    type: object
      properties:
        latitude:
        ...
        longitude::
        ...
   style: matrix
   explode: true

上記はOpenAPI3.0で新しくサポートされたPath-Style Parameterというセミコロン(;)でデータを区切る形式。

Swagger/OpenAPI2.0からOpenAPI3.0へ移行するには、変更点も多いためコンバーターを利用するのがよい。

コンバーターとしてMarmade SoftwareがOSSとして開発しているオンラインエディタコンバーター:Mermade Swagger 2.0 to OpenAPI3.0.0 converterがある。

変更する際の注意点として、フォームデータのパラメーター名が仕様上必ず欠落してしまう点がある。

この解決方法としてOpenAPI Generatorというコードジェネレーターがあり、このジェネレーターではOperationオブジェクトにx-codegen-request-body-nameという拡張フィールドを実装している。

OpenAPI3.0のスキーマでフォームデータのパラメーターを指定することで生成するクライアントコードの互換性を保つことができる。

%d人のブロガーが「いいね」をつけました。