Swagger ではない OpenAPI Specification 3.0 による API サーバー開発

10.5K Views

November 21, 19

スライド概要

JJUG CCC 2019 Fall の発表資料になります。
OpenAPI Generator を使って小規模な Web API サーバーを開発したときの経験やノウハウをまとめたものです。
https://ccc2019fall.java-users.jp/
https://jjug-cfp.cfapps.io/submissions/92e3117f-d911-4674-b97b-581813cfa0dc

profile-image

エンジニア・デザイナー向けのヤフー公式アカウント。イベント/登壇情報/ブログ記事など、ヤフーの技術・デザインに関わる情報を発信します。

シェア

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

Swaggerではない OpenAPI Specification 3.0 によるAPIサーバー開発 2019年11⽉20⽇ データ統括本部データソリューション事業本部 森本哲也 Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

2.

⾃⼰紹介 森本 哲也 (もりもと てつや) サーバーサイドエンジニア (7年) → SRE/インフラエンジニア (3年) • 2016 東京オフィス オブジェクトストレージDragonの運⽤/開発 (Go) • 2018 ⼤阪オフィス 検索ログの可視化ツールOtterのバックエンド開発 (Python, Java) ⾔語歴はPython → Java → Goをそれぞれ3-5年ほどの実務経験 プログラミングやOSS⽂化が好き Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 2

3.

⽬次 SwaggerとOpenAPIの間にある断絶 Spring Bootを使ったAPIサーバー開発 OpenAPI GeneratorとGradleプラグイン コード⽣成とそのカスタマイズ (オプション、タイプマッピング、テンプレート) ドキュメント⽣成と複数のspecファイル 実際に開発してよかったこと・わるかったこと spec(スキーマ)とインテグレーションの展望 Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 3

4.

SwaggerとOpenAPI の間にある断絶 Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

5.

Swagger CodegenとOpenAPI Generator Swagger Codegenの開発はSmartBear社が主導 コミュニティの分断 • コミュニティ軽視でビジネス主導のコミットがあり、開発コミュニティに不信感がたまっていく • 混乱をおさめるためトップコントリビューターのWilliam⽒が話し合い、全く聞き⼊れられず SmartBear社の説得を諦め、Swagger CodeGenをforkして別プロジェクトへ 2018年3⽉頃にOpenAPI Generatorとしてコミュニティ・ドリブンな開発体制で再始動 詳細は 平静を保ち、コードを生成せよ 〜 OpenAPI Generator誕生の背景と軌跡 〜 を参照 Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 5

6.

Swagger Codegen 実際にサンプルコードを⽣成してみた • github.com/t2y/swagger-springboot-sample OpenAPI Generatorの⽣成コードとの⽐較 CustomInstantDeserializerが2015-12-26にリリースされた Jackson Datatype ThreeTenBackport-2.6.4に依存 保守されてない︖ Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 6

7.

OpenAPI Generator 実際にサンプルコードを⽣成してみた • github.com/t2y/openapi-springboot-sample Swagger Codegenの⽣成コードとの⽐較 ControllerとAPI Interfaceを分離して⽣成される APIインターフェースはJava8から導⼊された デフォルト実装を使うようになっている Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. ⽣成コードの 構造が全く違う 7

8.

SwaggerとOpenAPIの調査 調べながらツイートしていたら William⽒からメールが届いたので 何往復かやり取りして教えてもらった Swagger Codegenについてのツイートを みつけたので連絡しています。 Swagger Codegenの経験について 教えてもらえないですか? 簡単に使えてカスタマイズできましたか? Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 8

9.

コードジェネレーターの開発状況 Swagger Codegen ⽉間コントリビューター10⼈未満 OpenAPI Generator fork後1.5年で ⽉間コントリビューター50⼈超 コード量が約2倍 https://www.openhub.net/ Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. fork fork fork 9

10.

OpenAPI Generatorリリース履歴 4.2.1: 4.2.0: 4.1.3: 4.1.2: 4.1.1: 4.1.0: 4.0.3: 4.0.2: 4.0.1: 4.0.0: 2019-11-15 2019-10-31 2019-10-04 2019-09-11 2019-08-26 2019-08-09 2019-07-09 2019-06-20 2019-06-01 2019-05-13 Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 直近のリリース履歴をみてみる • マイナーバージョンアップが2-3ヶ⽉ほど • パッチバージョンアップが0.5-1ヶ⽉ほど ⼗分にRapid Releaseな開発体制にみえる 10

11.

コードジェネレーターの選択 OpenAPI Generatorを選ぶ • Swagger Codegenの開発体制の不透明さ • Swagger Codegenの最近の開発は活発ではない • ⽣成コードの品質はOpenAPI Generatorの⽅が⾼い 新規開発にSwagger Codegenを選択する理由はないと私は思う Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 11

12.

Spring Bootを使った APIサーバー開発 Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

13.

ヤフーのデーソリューション事業 社内で利⽤中のデータ検索ツール 社外へリリース DS.INSIGHT People Otter Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. ⽣活者の興味関⼼を可視化 13

14.

システム構成 Otter API 社外向け ユーザー DS.INSIGHT DS.INSIGHT People FE People BE 社員が業務で利⽤ Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. Otter UI Cassandra 集計 データ このWeb APIサーバーを OpenAPI Gneratorを 使って開発した話 検索 システム ObjectStorage (Dragon) 14

15.

開発規模 OpenAPI Spec/Generator サーバー実装 (Spring Boot) • エンドポイント数: 23 • クラス数: 172 • specファイル数: 11 • ソースコード⾏数: 17,794 • spec⾏数: 4,755 ⼩規模なWeb API開発 • ⽣成クラス数: 70 • マイクロサービス • ⽣成コード⾏数: 21,660 • フルスクラッチ開発 • 開発期間: 約3ヶ⽉ Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 15

16.

Generation Gapパターンを採⽤ ⾃動⽣成されたソースコードには⼀切修正を⾏わず、 継承や委譲を使って⾃分たちのコードを実装する • ⽣成コードはリポジトリ管理しない • ⽣成コードと実装コードを明確に 分離することで保守コストを削減 (注意事項) OpenAPI Generatorの アップグレード時に⽣成コード の差分検証をしないといけない ツールのアップグレードで後述 • CI/CDとのインテグレーションが容易になる Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 16

17.

OpenAPIを⽤いた開発の流れ レスポンス 変えたい パラメーター不⾜ バリデーションエラー specを書く • エンドポイントを定義 (api interface) • パラメーターを定義 (model) • レスポンスを定義 (model) Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. コード⽣成 API 実装 テスト specを起点としてインターフェースを 調整する必要があるため、 開発中にみるところと作業の⼿間が増える ⼀連の作業に慣れが必要 17

18.

OpenAPI Specification (OAS) フォーマットはyamlまたはjsonのどちらでもよい 好み︖ • JSON Schemaの仕様をいくつか取り込んでいるのでほぼ互換性がある • 私はyamlを選択したが、フロントエンド開発で使うならjsonの⽅が親和性がある︖ 最新バージョンは 3.0.2 • Semantic Versioningを採⽤しているようにみえる → 次バージョンは3.0.3と3.1.0 • マイナーバージョンは互換性を壊さない、次の⾮互換変更は4.0.0になる︖ 仕様はOpenAPI Initiativeという⾮営利組織が策定 • OpenAPI Generatorの開発コミュニティとは無関係 Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 18

19.

JSON Schemaとの互換性 次の仕様はJSON Shema由来のもの • データ型 主要な機能をJSON Schemaから 取り⼊れているので必然的に似ている • Reference Object $refフィールドにより、複数ファイルで管理可能 • Schema Object (properties) フィールド定義⽅法やプロパティ継承の仕組み Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 19

20.

Reference Object paths: $refフィールドで /metrics/{frequency}/{frequencyDate}: get: 同じファイル内のオブジェクト summary: Query Metrics API または description: Returns single query metrics operationId: getQueryMetrics 外部ファイルのオブジェクトを参照 parameters: - $ref: '#/components/parameters/frequencyPath' - ... responses: 200: $ref: '#/components/responses/queryMetricsResponse' openapi.yaml components: parameters: frequencyPath: name: frequency in: path required: true schema: $ref: 'schemas.yaml#/components/schemas/frequency' Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. schemas.yaml 20

21.

Schema Object components: schemas: ⽂字列のenumを定義するスキーマオブジェクト frequency: description: time unit of aggregation process, ... type: string enum: [daily, monthly, yearly, weekly, latest_monthly, latest_yearly] frequencyDatePath: description: date path object for frequency, ... type: object properties: frequency: $ref: '#/components/schemas/frequency' year: type: integer Propertiesにメンバー(属性)を定義 month: $refフィールドで参照もできる type: integer ... schemas.yaml Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 21

22.

複数ファイルに分割 管理しやすいように 種別や⽤途にあわせて分割 • ⽤途の異なる定義を 外部ファイルに追い出せる 例) Example Objectの定義 • 巨⼤なオブジェクト定義を 外部ファイルに追い出せる 例) 1オブジェクト442⾏ spec ├── ├── ├── ├── ├── ├── ├── ├── ├── ├── └── プロダクションレベルだと定義が ⼗数ファイル、数千⾏といったオーダー examples.yaml (1433) openapi.yaml (939) schemas.yaml (770) schemas_age_metric.yaml (70) schemas_basic_metric.yaml (10) schemas_device_metric.yaml (10) schemas_gender_metric.yaml (55) schemas_metric.yaml (222) schemas_prefecture_metric.yaml (148) schemas_query_metrics_minutely_result.yaml (442) schemas_query_metrics_result.yaml (658) total 4757 lines Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. デモ 22

23.

シングルファイルに統合 ツールによっては1つのopenapi.yamlしか扱えないものもある • 例としてはredoc-cliはそう • $refフィールドはファイルシステムもURLも扱える ちゃんと制御するのは⾯倒なので同⼀ファイル内しか実装していないツールがある シングルファイルに統合するツールはいくつもみつかるが、 私たちの⽤途にあっているか調べるのが⾯倒で⾃分で作った • pypi.org/project/openapi-ext-tools/ Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. ドキュメント⽣成 のところで後述 23

24.

OpenAPI Generatorと Gradleプラグイン Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

25.

Gradleプラグイン 標準でgradleプラグインを提供 • openapi-generator-gradle-plugin openApiGenerateタスクにオプションを 設定することでコード⽣成の振る舞いを変更できる 複数のspecファイルに対応しているが、 複雑な参照構造は不可だったような気がする Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 後述 25

26.

タスクの流れ Javaコードのコンパイル前に次のタスクを追加して • specのバリデーション • specからコード⽣成 (specに変更があれば) コンパイル時に毎回コード⽣成する Generation Gapパターンを徹底 sourceSetsに⽣成したコードの場所を指定する build.gradle compileJava.dependsOn tasks.openApiValidate, tasks.openApiGenerate sourceSets.main.java.srcDir "${openApiGenerate.outputDir.get()}/src/main/java" sourceSets.main.resources.srcDir "${openApiGenerate.outputDir.get()}/src/main/resources" Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 26

27.

openApiValidate inputSpecにspecファイルの場所を指定する build.gradle openApiValidate { inputSpec = "${rootDir}/spec/openapi.yaml" } Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 27

28.
[beta]
openApiGenerate (1)
build.gradle

generatorNameで⽣成コード種別を指定
Generators List を参照
•

クライアントが60種別

•

サーバーが50種別

•

ドキュメントが7種別

•

スキーマが2種別

•

CONFIGが3種別

spring => Spring Boot

graphql-schema
protobuf-schema

Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

openApiGenerate {
generatorName = 'spring'
configFile = "${rootDir}/spec/config.json"
inputSpec = "${rootDir}/spec/openapi.yaml"
outputDir = "${buildDir}/generated"
configOptions = [
dateLibrary: 'java8'
]
systemProperties = [
modelDocs: 'false'
]
typeMappings = [...]
コード⽣成の
カスタマイズで後述
importMappings = [...]
skipValidateSpec = true
logToStderr = true
generateAliasAsModel = false
}

28

29.
[beta]
openApiGenerate (2)
build.gradle

configFileは、
generatorNameで指定した
⽣成コード種別 (spring) に
特化した設定を指定する

openApiGenerate {
generatorName = 'spring'
configFile = "${rootDir}/spec/config.json"
inputSpec = "${rootDir}/spec/openapi.yaml"
outputDir = "${buildDir}/generated”
...

spec/config.json

{

"packageName": "jp.co.yahoo.u2.otter",
"apiPackage": "jp.co.yahoo.u2.otter.api.spec",
"modelPackage": "jp.co.yahoo.u2.otter.model.spec",
"interfaceOnly": true
}

Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

⽣成コードオプションで後述

29

30.

コード⽣成オプション Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

31.
[beta]
springのコード⽣成オプション
具体的なオプションは
ドキュメントを参照
• Config Options for spring
⽣成コードに影響するオプション
• interfaceOnly: (Boolean)

サンプルコード
github.com/t2y/openapi-springboot-sample
{

spec/config.json

"interfaceOnly": false,
"delegatePattern": false
}

• delegatePattern: (Boolean)

Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

31

32.

デフォルトの⽣成コード generated/src/main/java └── org └── openapitools ├── api │ ├── ApiUtil.java │ ├── BooksApi.java │ ├── BooksApiController.java (Generation Gapパターンでは) ├── PingApi.java UsersApiControllerを継承して│ │ ├── PingApiController.java APIのインターフェースの │ ├── StreamApi.java メソッドをオーバーライド︖ │ ├── StreamApiController.java │ ├── UsersApi.java │ └── UsersApiController.java ├── configuration │ ├── HomeController.java │ └── OpenAPIDocumentationConfig.java ├── invoker │ ├── OpenAPI2SpringBoot.java │ └── RFC3339DateFormat.java Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 32

33.

interfaceOnly=trueの⽣成コード (Generation Gapパターンでは) UsersApiControllerを作成して APIインターフェースのメソッドを実装する Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. generated/src/main/java └── org └── openapitools ├── api │ ├── ApiUtil.java │ ├── BooksApi.java │ ├── PingApi.java │ ├── StreamApi.java │ └── UsersApi.java 最も⽣成コードが少ない 33

34.

delegatePattern=trueの⽣成コード generated/src/main/java └── org └── openapitools ├── api │ ├── ApiUtil.java │ ├── BooksApi.java │ ├── BooksApiController.java (Generation Gapパターンでは) │ ├── BooksApiDelegate.java UsersApiDelegateImplを作成して │ ├── PingApi.java ├── PingApiController.java APIインターフェースのメソッドを実装する︖ │ │ ├── PingApiDelegate.java │ ├── StreamApi.java │ ├── StreamApiController.java │ ├── StreamApiDelegate.java │ ├── UsersApi.java │ ├── UsersApiController.java │ └── UsersApiDelegate.java ├── configuration │ ├── HomeController.java │ └── OpenAPIDocumentationConfig.java ├── invoker │ ├── OpenAPI2SpringBoot.java DelegateでDIできるけど冗⻑過ぎ︖ │ └── RFC3339DateFormat.java Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 34

35.

(まとめ) springのコード⽣成オプション spec/config.json { 私たちの⽤途では interfaceOnlyで⼗分に思えた • Generation Gapパターン • マイクロサービスなWeb API Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. "interfaceOnly": true, } インターフェースのみ⽣成を選択 ・クラスの継承構造は不要 ・クラスの委譲構造は冗⻑ 35

36.

(Spring Boot向けに) コード⽣成とタイプマッピング Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

37.

CLIの基本的な使い⽅ インストールはjarをダウンロードするだけ command line $ VERSION=4.2.1 $ URI="http://central.maven.org/maven2/org/openapitools/openapi-generator-cli/${VERSION}/openapigenerator-cli-${VERSION}.jar" $ curl -L ${URI} -o openapi-generator-cli-${VERSION}.jar コード⽣成 $ java -jar openapi-generator-cli-4.2.1.jar generate ¥ --input-spec openapi.yaml ¥ --output ./generated ¥ 最も簡単なのは3つだけ指定すればいい --generator-name spring • • • Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. specファイル ⽣成コードの出⼒先 ⽣成コードの⾔語/種別 37

38.
[beta]
interfaceOnly=trueの⽣成コード
⽣成されたあるAPIインターフェースのdefault実装
•

Spring Bootのフレームワークにあわせたアノテーションが付与されたメソッドのコードが⽣成される

@ApiOperation(value = "Query Metrics API", nickname = "getQueryMetrics", notes = "Returns single query metrics", response = QueryMetricsData.class, tags = {})
@ApiResponses(value = { @ApiResponse(code = 200, message = "query metrics response", response = QueryMetricsData.class) })
@RequestMapping(value = "/metrics/{frequency}/{frequencyDate}", produces = { "application/json", "text/csv", "text/tab-separated-values" }, method = RequestMethod.GET)
default ResponseEntity<QueryMetricsData> getQueryMetrics(
@ApiParam(value = "", required = true, allowableValues = "¥"daily¥", ¥"monthly¥", ¥"yearly¥", ¥"weekly¥"") @PathVariable("frequency") Frequency frequency,
@ApiParam(value = "", required = true, defaultValue = "null") @PathVariable("frequencyDate") FrequencyDatePath frequencyDate,
@NotNull @ApiParam(value = "", required = true) @Valid @RequestParam(value = "query", required = true) String query,
@ApiParam(value = "") @Valid @RequestParam(value = "basic", required = false) List<BasicMetric> basic,
@ApiParam(value = "") @Valid @RequestParam(value = "device", required = false) List<DeviceMetric> device,
@ApiParam(value = "") @Valid @RequestParam(value = "gender", required = false) List<GenderMetric> gender,
@ApiParam(value = "") @Valid @RequestParam(value = "age", required = false) List<AgeMetric> age,
@ApiParam(value = "") @Valid @RequestParam(value = "pref", required = false) List<PrefectureMetric> pref,
@ApiParam(value = "", allowableValues = "json, csv, csv_jp, tsv, tsv_jp", defaultValue = "json")
@Valid @RequestParam(value = "format", required = false, defaultValue = "json") Format format) {
getRequest().ifPresent(request -> {
for (MediaType mediaType : MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
ApiUtil.setExampleResponse(request, "application/json", "{ ... }");
break;
}
}
});
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}

MetricsApi.java

デモ
Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

38

39.
[beta]
メソッドの引数と返り値の詳細
@RequestMappingはSpring Bootのアノテーション
MetricsApi.java 抜粋

@RequestMapping(value = "/metrics/{frequency}/{frequencyDate}",
produces = { "application/json", "text/csv", "text/tab-separated-values" },
method = RequestMethod.GET)
default ResponseEntity<QueryMetricsData> getQueryMetrics(
@ApiParam(value = "", required = true,
allowableValues = "¥"daily¥", ¥"monthly¥", ¥"yearly¥", ¥"weekly¥"")
@PathVariable("frequency") Frequency frequency,
...
) {
specで定義したパラメーターの
...
Frequencyの変換もやってくれる︖
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
→ やってくれない︕

ResponseEntityとしてspecで定義したレスポンスを返す

Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

39

40.
[beta]
パラメーターのオブジェクトマッピング
Converterを定義してリクエストパラメーターの⽂字列
からオブジェクトへの型変換を実装しないといけない

OpenAPI Generator側で
⽣成してくれてもいいのかも︖

@Component
public class FrequencyConverter implements Converter<String, Frequency> {
@Override
public Frequency convert(String source) {
try {
return Frequency.fromValue(source);
} catch (Exception e) {
throw new ConversionException(
Frequency.class.getSimpleName(), source);
}
}
}

Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

FrequencyConverter.java

40

41.

任意の型とのマッピング CSV/TSVダウンロード向けにストリームを返すAPIを実装したい Spring Bootではストリーム向けに2つのリソースを提供している • InputStreamResource: 汎⽤リソース • ByteArrayResource: 任意のByteArray(画像ファイルとか) ⼀⽅でOpenAPIではspecでAPIのパラメーターとレスポンスを 定義しなければいけない Spring Bootの型をどうやってspecファイルで表現すればいいか︖ Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 41

42.

OpenAPI Generatorのオプション Type Mappings and Import Mappings • --type-mapping: マッピングしたい型を書く • --import-mapping: テンプレート向けに必要︖ このオプションを使うことで任意の型をマッピングできる Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 42

43.

Gradleプラグインでのオプション Spring BootがもつInputStreamResourceというクラスにinputStreamというオブジェクト名でマッピング openApiGenerate { generatorName = 'spring' ... typeMappings = [ inputStream: 'org.springframework.core.io.InputStreamResource', httpStatus: 'org.springframework.http.HttpStatus', errorCode: 'jp.co.yahoo.u2.otter.constant.ErrorCode', errorUrl: 'jp.co.yahoo.u2.otter.constant.ErrorUrl', ] importMappings = [ inputStream: 'org.springframework.core.io.InputStreamResource', httpStatus: 'org.springframework.http.HttpStatus', errorCode: 'jp.co.yahoo.u2.otter.constant.ErrorCode', errorUrl: 'jp.co.yahoo.u2.otter.constant.ErrorUrl', ] ... } build.gradle Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 43

44.

マッピングするときのspecの定義 openapi.yaml components: responses: downloadResponse: description: query metrics download data content: text/csv: schema: $ref: 'schemas.yaml#/components/schemas/inputStream' text/tab-separated-values: schema: $ref: 'schemas.yaml#/components/schemas/inputStream' application/zip: schema: $ref: 'schemas.yaml#/components/schemas/inputStream' inputStreamの スキーマ定義は必要 schemas.yaml components: schemas: inputStream: type: object properties: body: type: string format: binary ストリームを返したいのでそれっぽいオブジェクトを定義する コード⽣成時にinputStreamオブジェクトは使われないが、 ドキュメントはこの定義に従って⽣成される Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 44

45.
[beta]
マッピングするときの⽣成コード
@RequestMapping(value = "/download/metrics",
produces = { "text/csv", "text/tab-separated-values", "application/zip" },
consumes = { "application/json" },
method = RequestMethod.POST)
default ResponseEntity<org.springframework.core.io.InputStreamResource>
downloadQueryMetrics(...) {
getRequest().ifPresent(request -> {
ResponseEntityの型引数として
for (MediaType mediaType:
MediaType.parseMediaTypes(request.getHeader("Accept")))
{
InputStreamResourceが指定される
if (mediaType.isCompatibleWith(MediaType.valueOf(""))) {
ApiUtil.setExampleResponse(request, "", "");
break;
}
}
});
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

45

46.

マッピングするときのドキュメント コード⽣成に 使われないので propertiesを 定義しなくてもよい schemas.yaml components: schemas: inputStream: type: object schemas.yaml bodyというフィールドを もつわけではないけど、 それっぽくみえる︖ Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. components: schemas: inputStream: type: object properties: body: type: string format: binary 46

47.

数値をもつenum OASでは数値をもつenumに名前をつける仕様がない • github.com/OAI/OpenAPI-Specification/issues/681 例えば、エラーコードを定義する場合 components: schemas: errorCode: description: code in error type: integer enum: - 0 - 40000 - 40001 - 40100 schemas.yaml Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. public enum ErrorCode { NUMBER_0(0), NUMBER_40000(40000), NUMBER_40001(40001), NUMBER_40100(40100), ... } ErrorCode.java 名前がもったいない 47

48.

(補⾜) ⽂字列をもつenum ⽂字列をもつenumであれば問題ない schemas.yaml components: schemas: frequency: description: time unit of ... type: string enum: [daily, monthly, yearly, weekly, ...] public enum Frequency { DAILY("daily"), MONTHLY("monthly"), YEARLY("yearly"), WEEKLY("weekly"), ... } Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. frequency.java 48

49.

任意のenumをマッピング ⾃分で定義したErrorCodeというenumをerrorCodeというオブジェクト名でマッピング openApiGenerate { generatorName = 'spring' ... typeMappings = [ errorCode: 'jp.co.yahoo.u2.otter.constant.ErrorCode', ] importMappings = [ errorCode: 'jp.co.yahoo.u2.otter.constant.ErrorCode', ] ... } build.gradle エラーコードのような仕様として不変の値とするものは数値を使いたかった (後述) Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 49

50.

数値をもつenumに名前付け enumの数値に名前をつけることはできる components: schemas: errorCode: description: code in error type: integer enum: - 0 - 40000 - 40001 - 40100 schemas.yaml Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. public enum ErrorCode { UNCATEGORIZED(0), INVALID_PATH_OR_PARAMETER(40000), OVER_PARAMETER_LIMIT(40001), NO_AUTH_HEADER(40100), ... } ErrorCode.java (注意) specとソースコードの⼆重管理が必要 specからドキュメントと実装に乖離がないことが OpenAPIの利点であるのにそれを台無ししてしまう OASとして対応を期待 50

51.
[beta]
(余談) エラーレスポンス
ぼくのかんがえたさいきょうのえらーれすぽんす
HTTP ステータスコード: 503
{

エラーレスポンス

"code": 50300, ← 不変の値
↓ 例外のエラーメッセージ
"type": "CassandraReadTimeoutException", ← 例外の型
"message": "Cassandra timeout during read query at consistency LOCAL_QUORUM",
"service": "cassandra", ← エラーが発⽣したコンポーネント/サービス
"url": "http://example.com/K-DfQd" ← 発⽣したエラーに関してみるべきドキュメントのURL

}

例えば、500のときは問い合わせフォーム

HTTP ステータスコードと、レスポンスのcodeの2つを仕様として保証
その他の情報は参考情報 (将来、変わる可能性がある)
→ 参考とはいえ、これらがあるとエラーの場所と原因を特定しやすい

Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

51

52.

(余談) エラーレスポンスのリファレンス これらのブログ記事を参考にしました • REST API Error Codes 101 • REST API Error Handling - Problem Details Response • Best Practices for API Error Handling Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 52

53.

(Spring Boot向けに) コード⽣成とテンプレート Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

54.

テンプレート OpenAPI Generatorが⽣成するコードそのものを カスタマイズしたいときはテンプレートを使う • Using Templates Mustacheというテンプレート⾔語でコード⽣成している • 既存のテンプレートをコピーして書き換えることで コード⽣成をカスタマイズできる • 但し、Mustacheの学習コストは必要 Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 54

55.

Spring Boot向けのテンプレート Spring Boot向けのコード⽣成のテンプレートは次の場所にある • github.com/OpenAPITools/openapi-generator/tree/master/modules/openapigenerator/src/main/resources/JavaSpring 最も簡単なコード⽣成のカスタマイズ⽅法として、 既存のテンプレートからカスタマイズしたいコードを含む テンプレートをコピーして必要なところを書き換える command line $ cp path/to/openapi-generator/modules/openapigenerator/src/main/resources/JavaSpring/typeInfoAnnotation.mustache templates/ $ vi templates/typeInfoAnnotation.mustache Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 55

56.
[beta]
簡単なテンプレートのカスタマイズ
既存のtypeInfoAnnotation.mustacheをカスタマイズ
• ⽬的: Jacksonのシリアライズの振る舞いを変えたい
サンプルコード
github.com/t2y/issue-generation-with-allof
build.gradle

openApiGenerate {
generatorName = 'spring'
templateDir = "${rootDir}/templates"
...
}

Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

(注意)
リポジトリにコミットしてある
build.gradleはtemplateDirを
コメントアウトした状態

56

57.

Discriminator Object モデルの⽣成コードで継承関係を保持するために必要 components: schemas: Pet: type: object properties: name: type: string age: type: integer discriminator: propertyName: petType mapping: myDog: Dog myCat: Cat Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 詳細はリポジトリ のREADMEにある issueを参照 Cat: allOf: - $ref: '#/components/schemas/Pet' - type: object properties: house: type: string Dog: allOf: - $ref: '#/components/schemas/Pet' - type: object properties: bark: type: string openapi.yaml 57

58.

Petの⽣成コード @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "petType", visible = true) @JsonSubTypes({ @JsonSubTypes.Type(value = Dog.class, name = "myDog"), @JsonSubTypes.Type(value = Cat.class, name = "myCat"), }) public class Pet { Discriminator ObjectのpropetyNameは @JsonProperty("name") private String name; @JsonTypeInfoとして定義される Petクラスの属性としては定義されない @JsonProperty("age") private Integer age; ... } Pet.java Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 58

59.

Petを継承するオブジェクト 例えば、Petを継承するCatオブジェクトをシリアライズする • Discriminator ObjectのpropertyNameに定義された petTypeもJacksonによりシリアライズされてしまう { "petType": "myCat", "name": "kuro", "age": 1, "house": "myhome" } Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. petTypeが不要なときは⾮効率 cat.json 59

60.

typeInfoAnnotation.mustache オリジナルのtypeInfoAnnotation.mustacheが次の通り • include = JsonTypeInfo.As.PROPERTYが固定 typeInfoAnnotation.mustache {{#jackson}} @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true) @JsonSubTypes({ {{#discriminator.mappedModels}} @JsonSubTypes.Type(value = {{modelName}}.class, name = "{{^vendorExtensions.x-discriminatorvalue}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.xdiscriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.xdiscriminator-value}}"), {{/discriminator.mappedModels}} テンプレートを変更して }) {{/jackson}} includeのパラメーターを次に変更する include = JsonTypeInfo.As.EXISTING_PROPERTY Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 60

61.

Jacksonの振る舞いのカスタマイズ テンプレートのincludeを書き換えてコード⽣成することで Jacksonのシリアライズを意図した振る舞いに変更できた Pet.java @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "petType", visible = true) { "name": "kuro", "age": 1, "house": "myhome" petTypeの属性が不要なら シリアライズされなくなった } Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. cat.json 61

62.

(まとめ) テンプレートのカスタマイズ ツールのアップグレードやテンプレートの保守を考慮すると、 テンプレートのカスタマイズは控えるべき︕ カスタマイズ⼿段が しかし、どうしても必要なときはやるしかない あること⾃体は⼤事 typeInfoAnnotation.mustacheは Discriminator Objectを指定したときしか使われていない ようにみえたのでパラメーターを変更することにした Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 62

63.

ドキュメント⽣成と 複数のspecファイル Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

64.

CIでドキュメント⽣成を⾃動化 GitHub Enterprise GitHub Pages 1. openapi-ext-toolsで複数specを1つに統合 Screwdriver.cd 2. 統合specからredoc-cliでsingle htmlを⽣成 3. single htmlをGitHub Pagesにpush specを修正 Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 64

65.

ドキュメント⽣成の関連ツール redoc-cliは複数のspecファイルを扱えない • 複数のspecファイルを1つに統合するために openapi-ext-toolsというツールを作った command line $ openapi-spec-cli --spec-path otter-api/spec/openapi.yaml ¥ --bundled-spec-path bundled_openapi.yaml $ redoc-cli bundle bundled_openapi.yaml --output index.html ¥ --options.expandResponses=all --options.requiredPropsFirst=true ¥ --options.jsonSampleExpandLevel=10 --options.hideLoading=true ¥ --options.pathInMiddlePanel=true Prerendering docs 🎉 bundled successfully in: index.html (1685 KiB) [ 0.917s] Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 65

66.

⽣成したドキュメントの⾒た⽬ 私が検証した中では redocが⾒た⽬がよく 他システムとの インテグレーションが 容易に思えた 右サイドバー にレスポンス のサンプル 左サイドバー にAPI⼀覧 デモ Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 66

67.
[beta]
Example Object
パラメーターやレスポンスのサンプルデータとして
ドキュメントで表⽰したい値を定義する
components:

components:
examples:
yamlの⽂法として
statusInfo:
responses:
キーバリューもそのまま書ける
value:
gslbResponse:
{
description: gslb response
"status": "OK",
"instance": "192.168.1.1",
content:
"date": "2019-10-25T16:13:23.18+09:00"
application/json:
}
schema:
examples.yaml
$ref: 'schemas.yaml#/...'
examples:
statusInfo:
$ref: 'examples.yaml#/components/examples/statusInfo'
デモ

Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

67

68.

実際に開発して よかったこと・わるかったこと Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

69.

よかったこと ドキュメント⾃動⽣成は開発途中における 他チームとの連携においてFEチームから好評だった テストやCI/CDなどと相性がよい • インテグレーション モックサーバー構築、テストクライアント⽣成、多⾔語対応 (慣れれば) specはコピペで書けるので ボイラープレートなコーディングは⽣産性が上がる︖ Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 69

70.

わるかったこと OpenAPIそのものの学習コストが⾼い • ちょっと開発を⼿伝っては不可 specとコード⽣成とそれらを使う設計・実装を理解するのに1ヶ⽉ぐらいかかる APIインタフェースの制約が設計に与える影響は⼤きい • 妥協しない施策 タイプマッピング テンプレート ⼊出⼒とそのインタフェースが OpenAPI Generatorに依存するため、 フレームワークの設計思想や プログラミングパラダイムとの間に妥協を強いられる Go⾔語 (go-gin-server) も試してみたが、これは ちょっと使えないと思った github.com/t2y/openapi-gin-sample OpenAPI Generatorのバグに2回遭遇してバージョンを上げられなかった • 放置されるわけではないが、 コードジェネレーターは範囲が広いので保守できる開発者不⾜にみえる Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 70

71.

OpenAPIと学習コストとエコシステム specからどんなコードが⽣成されるのか ⽣成されたコードをどう扱うのか OpenAPIはその性質上、 公開インターフェースを提供するため、 必然的にプロダクトのエコシステム についても考えていくことになる • どう拡張するのか • フレームワークはどう連携するのか • どうパッケージングするのか • コード⽣成のカスタマイズはどうバランスをとるのか CI/CDとどのようにインテグレーションするのか プロダクトの開発⽅法論や開発プロセスをどうするか プロダクトの外部とのインテグレーションはどう保証するのか Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 71

72.

ツールのアップグレード APIインターフェースを実装するcontrollerは ⽣成コードからcontrollerにメソッド定義をコピペしている specのAPI定義を変更/追加しない限り、 ⽣成コードからメソッド定義を コピペし直すことはない $ $ $ $ $ アップグレードのタイミングと リグレッションの発⽣タイミングが 別々になる可能性がある command line gradle clean openapiGenerate # 現行バージョンでビルド cp -pr build/generated before-generated # 現行バージョンのソース vi build.gradle # アップグレード 4.0.1 -> 4.2.1 gradle clean openapiGenerate # 新しいバージョンでビルド diff -ur before-generated/ build/generated # 生成されたソースコードの差分を確認 アップグレード時にコード差分を必ずチェック︕ Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 72

73.

遭遇した不具合とその対応 ツールのアップグレードで遭遇した不具合 4.0.1の次は4.2.1にアップグレード タイミング悪く5ヶ⽉待つことに (´・ω・`) github.com/OpenAPITools/openapi-generator/issues/3248 • API interfaceに@RequestParamのアノテーションがつかない • 2019-06-29 open, 09-16 fix, 10-04 release (4.1.3) 4.0.2 4.1.2 4.1.1 4.2.0 github.com/OpenAPITools/openapi-generator/issues/3815 • openApiGenerateタスクにconfigFileを指定したときにビルドできない • 2019-09-02 open, 11-11 fix, 11-15 release (4.2.1) Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 73

74.

まとめ Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.

75.

まとめ OpenAPI GeneratorはProduction Ready 特にJava! • プロダクト開発を中⻑期で考えるときにスキーマ があることはインテグレーションの幅が広がる • OpenAPI Toolsの開発コミュニティは活発 宣⾔的な開発⽅法論が主流になりつつ中、 スキーマ駆動開発はいまの開発者にとっては⾃然︖ システム間連携の狙い Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 75

76.

リファレンス OpenAPI-Specification OpenAPI Generator • OpenAPI Generator Documentation 平静を保ち、コードを⽣成せよ 〜 OpenAPI Generator誕⽣の背景と軌跡 〜 Swagger Specから静的なHTMLを作るHTMLジェネレータを⾊々ためしてみた Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved. 76

77.

EOP ご清聴ありがとうございました Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.