The OpenAPI Specification (OAS)
OAS はHTTP API の仕様を記述するためのフォーマットです。
主な内容は以下のように、
- OAS のバージョン番号
- 記述するAPI のバージョン番号、タイトルなどの情報
- サーバへの接続情報
- 各エンドポイントの情報
からなります。(より詳しい書き方は https://swagger.io/specification/ を参照)
openapi: 3.0.3
info:
version: 0.0.0
title: OpenAPI Generator test with Qiita API
servers:
- url: https://qiita.com/api/v2/
paths:
/users:
get:
parameters:
- ...
responses:
200:
...
OAS は特定のプログラミング言語に依存しない形で記述できるため、API のドキュメントとしてだけではなく、そこからクライアント・サーバそれぞれのコードを生成することができます。
OpenAPI Generator(github)はOAS の形式で書かれたyaml やjson ファイルからコード生成を自動でやってくれるソフトウェアです。
今回はQiita API を題材にして、それを叩くAndroid アプリを作ります。
完成品
コマンドを叩くだけで以下のようなファイルが自動生成されるようになります。
UserApi.kt
import ...
interface UserApi {
/**
* 全てのユーザの一覧を作成日時の降順で取得します。
* getAllUser の詳細です。
* Responses:
* - 200: 取得成功です。
* - 0: Unexpected error
*
* @param page ページ番号 (1から100まで) (optional)
* @param perPage 1ページあたりに含まれる要素数 (1から100まで) (optional)
* @return [Call]<[kotlin.collections.List<User>]>
*/
@GET("users")
fun getAllUser(@Query("page") page: kotlin.String? = null, @Query("per_page") perPage: kotlin.String? = null): Call<kotlin.collections.List<User>>
...
}
User.kt
import ...
/**
* Qiita上のユーザを表します。
* @param description 自己紹介文
* @param facebookId Facebook ID
* @param ...
*/
data class User (
/* 自己紹介文 */
@Json(name = "description")
val description: kotlin.String? = null,
/* Facebook ID */
@Json(name = "facebook_id")
val facebookId: kotlin.String? = null,
...
}
環境
Android studio: 4.0.1
OpenAPI Generator: 5.0.0-beta2
仕様書を作る
Qiita API に対応するファイルが見つからなかったので、練習も兼ねて自分で書いていくことにしました。
ドキュメントからyaml ファイルを作っていきますが、全部のAPI を書くのは辛いので、今回は以下のAPI に絞りました。
GET /api/v2/users
GET /users/{user_id}
GET /api/v2/items
POST /api/v2/items
このファイルの作成には Swagger Editor という便利なサイトがあるので活用していきます。
このサイトでは、yaml を編集すると、逐次右側の画面が更新され、正しく解釈されているかを確認できたり、実際にAPI を叩いて結果を確認したりすることが可能です。
tags
まず、各API をグルーピングします。これはOAS でのtag
に対応します。
tag
は以下のようにyaml ファイルのルート に記述し、後述する各path の中でも指定します。
tags:
- name: user # api のインターフェース名になる
description: ユーザに関係する操作
- name: item
description: 投稿に関係する操作
paths:
/users:
get:
tags:
- user
parameters:
- ...
このようにグルーピングすることで、OpenAPI Generator から生成されるAPI 用のinterface を記述するファイルが分けられます。
components
schemas
次に各API から返ってくるデータの詳細を記述します。これはOAS でのcomponents.schemas
に対応します。
components
は以下のようにyaml ファイルのルート に記述し、その一階層下にschemas
を記述します。これも、後述するpath の中で指定することで、各API に対応付けることができます。
components:
schemas:
User: # モデルクラス名になる
description: Qiita上のユーザを表します。
type: object
properties:
description:
description: 自己紹介文
type: string
nullable: true
example: Hello, world.
...
これらはOpenAPI Generator ではモデルクラスに対応します。
securitySchemes
こちらはHTTP 認証やAPI キー、OAuth2 などのセキュリティ情報の詳細を記述します。これはOAS でのcomponents.securitySchemes
に対応します。
securitySchemes
は上記のschemas
と同じ階層に記述し、これも後述するpath の中で指定することで、各API に対応付けることができます。
components:
securitySchemes:
Bearer:
type: http
scheme: bearer
description: Access token for API
paths
最後に各API の情報を記述していきます。これはOAS でのpaths
に対応します。
paths
は以下のようにyaml ファイルのルート に記述します。
paths:
/users: # エンドポイント
get: # HTTP メソッド
tags:
- user # ここで指定するtag に対応するapi ファイルに振り分けられる
summary: 全てのユーザの一覧を作成日時の降順で取得します。
description: getAllUser の詳細です。
operationId: getAllUser # api ファイルでのメソッド名になる
parameters:
- name: page
in: query
description: ページ番号 (1から100まで)
example: 1
required: false
schema:
type: string
pattern: /^[0-9]+$/
- name: per_page
in: query
description: 1ページあたりに含まれる要素数 (1から100まで)
example: 20
required: false
schema:
type: string
pattern: /^[0-9]+$/
responses:
200:
description: 取得成功です。
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User' # スキーマを指定
default:
description: Unexpected error
paths
の下にはエンドポイント、そのさらに下にHTTP method を記述し、その下に詳細を記述します。
細かい仕様は https://swagger.io/specification/ を参照してください。
出来上がった openapi.yaml
ファイルはプロジェクトのルートに置いておきます。
Gradle の記述
Gradle を使って自動生成ができるようにします。
Gradle での記述方法は OpenAPI Generator Gradle Plugin に詳しく載っています。
今回はapp モジュールとは別にapi モジュールを作り、そこで自動生成されたファイルをapp モジュールから参照する形にします。
plugins {
kotlin("jvm")
// OpenAPI Generator
id("org.openapi.generator") version "5.0.0-beta2"
id("kotlinx-serialization")
}
dependencies {
implementation(fileTree("dir" to "libs", "include" to listOf("*.jar")))
// kotlin, retrofit, moshi などの依存
implementation(Dependency.kotlinStdLibJdk8)
implementation(Dependency.retrofit)
implementation(Dependency.moshi)
...
}
// API 名
val apiName = "qiita"
// ビルド先ディレクトリ
val buildApiDir = "$buildDir/openApiGenerator/$apiName"
// 自動生成先のパッケージ名
val basePackage = "com.example.openapigenerationtestapi"
fun String.packageToDir() = replace('.', '/')
task<org.openapitools.generator.gradle.plugin.tasks.GenerateTask>("generate") {
doFirst {
delete(file(buildApiDir))
}
// OpenAPI Generator のオプションを指定
generatorName.set("kotlin")
library.set("jvm-retrofit2")
// templateDir.set("$rootDir/template") // テンプレートを指定、詳細は後述
inputSpec.set("$rootDir/openapi.yaml")
outputDir.set(buildApiDir)
packageName.set(basePackage)
apiPackage.set("$basePackage.$apiName.api")
modelPackage.set("$basePackage.$apiName.model")
configOptions.set(mapOf(
"dateLibrary" to "java8"
))
additionalProperties.set(mapOf(
// ここでRx やCoroutine を利用するか指定できます
"doNotUseRxAndCoroutines" to "true"
))
generateApiTests.set(false)
}
task<Copy>("copy") {
val dirFrom = "$buildApiDir/src/main/kotlin/${basePackage.packageToDir()}/"
val dirInto = "$projectDir/src/main/java/${basePackage.packageToDir()}/"
doFirst {
delete(file(dirInto))
}
dependsOn("generate")
from(dirFrom)
into(dirInto)
}
task("buildApi") {
dependsOn("generate", "copy")
}
generate
がbuild ディレクトリに自動生成を行い、copy
でsrc 以下に必要なファイルをコピーします。
コマンドラインからは buildApi
を実行することでこれらをまとめて実行するようにします。
./gradlew :api:buildApi
これを実行すると、自動生成されたファイルを見ることができます。
テンプレートの修正
OpenAPI Generator も完璧ではありません。
例えば、v5.0.0-beta2
では自動生成される ApiClient.kt
ファイルで最新版のRetrofit2 に対してのエラーが発生します。
これをテンプレートを修正することで解決してみます。(この問題は現在 PR が出されており、次の v5.0.0
では修正されると思います。1)
まず、プロジェクトのルートに template
ディレクトリを作り、OpenAPI Generator にある テンプレート をダウンロードして配置します。
ApiClient.kt
に対応するファイルが libraries/jvm-retrofit2/infrastructure/ApiClient.kt.mustache
なのでこれを編集していきます。
...
fun <S> createService(serviceClass: Class<S>): S {
# REMOVE these lines
# var usedClient: OkHttpClient? = null
# this.okHttpClient?.let { usedClient = it } ?: run {usedClient = clientBuilder.build()}
# ADD this line
val usedClient: OkHttpClient = this.okHttpClient ?: clientBuilder.build()
return retrofitBuilder.client(usedClient).build().create(serviceClass)
}
...
この変更を加えたのち、Gradle の記述
の部分でコメントアウトしていたテンプレートの指定を行い、再生成を行うことで正しいファイルが生成できます。
今回はOAS の内容に依らない修正だったのでそのままkotlin を書くように編集するだけでしたが、OAS の内容に合わせて変化するコードを記述することも可能です。
詳しい内容は https://openapi-generator.tech/docs/templating/ を参照してください。
実際に呼ぶ部分の記述
ここまでこれば、あとはアプリを作っていくだけです。
ApiClient.kt
などの便利なクラスを利用することでかなり楽に書くことができます。
val userApi: UserApi = ApiClient(authName = "Bearer", bearerToken = "[MY_TOKEN]")
.createService(UserApi::class.java)
fun getAllUser(callback: Callback<List<User>>, page: Int? = null, perPage: Int? = null) {
val call = userApi.getAllUser(page?.toString(), perPage?.toString())
call.enqueue(callback)
}
...
Coroutine 対応コードを生成する
上ではAPI インタフェースは retrofit2.Call
を返す関数を定義しています。
そのため、自動生成以外の自分で書くコードではCallback を定義する必要があり、少し面倒です。
ここでは、Coroutine で使えるようなsuspend 関数を返すように変更します。
additionalProperties.set(mapOf(
// REMOVE this line
// "doNotUseRxAndCoroutines" to "true"
// ADD this line
"useCoroutines" to "true"
))
以上です。ありがたいことにこれだけでCoroutine で使えるコードが生成されます。
ItemApi.kt
interface ItemApi {
/**
* 記事の一覧を作成日時の降順で返します。
* getAllItems の詳細です。
* Responses:
* - 200: 取得成功です。
*
* @param page ページ番号 (1から100まで) (optional)
* @param perPage 1ページあたりに含まれる要素数 (1から100まで) (optional)
* @param query 検索クエリ (optional)
* @return [kotlin.collections.List<Item>]
*/
@GET("items")
suspend fun getAllItems(@Query("page") page: kotlin.String? = null, @Query("per_page") perPage: kotlin.String? = null, @Query("query") query: kotlin.String? = null): Response<kotlin.collections.List<Item>>
/**
* 新たに記事を作成します。
* postItem の詳細です。
* Responses:
* - 201: 作成成功です。
* - 0: Unexpected error
*
* @param inlineObject
* @return [Unit]
*/
@POST("items")
suspend fun postItem(@Body inlineObject: InlineObject): Response<Unit>
}
あとは、実際の呼び出し側も修正します。
suspend fun getAllItems(page: Int? = null, perPage: Int? = null, query: String? = null)
= itemApi.getAllItems(page?.toString(), perPage?.toString(), query)
終わりに
OpenAPI Generator はクライアント側だけでなく、サーバー側のコードも自動生成することができるので、ドキュメントとしてOAS に沿ったyaml ファイルを作っておくだけで、クライアント・サーバーで一貫性の取れたコードを書くことができます。
また、API に変更があれば、yaml ファイルに変更を適用し、再生成を行うだけですぐにAPI を利用することができるため、開発の負担を大きく減らすことが期待できます。
SwaggerEditor で簡単に試すことができるので、OpenAPI Generator をやってみてはいかがでしょうか。
参考
https://tech-blog.optim.co.jp/entry/2020/04/13/100000
https://techblog.asahi-net.co.jp/entry/2019/03/04/102734
https://qiita.com/amuyikam/items/e8a45daae59c68be0fc8
https://qiita.com/sengok/items/1d958348215647a5eaf0
今回作成したサンプルプロジェクト
https://github.com/warahiko/OpenApiGenerationTest