🫖

Kotlin(Spring Boot)の API を OpenAPI Generator で自動生成

2022/10/21に公開

概要

本記事では OpenAPI Generator を用いて openapi.yaml から Kotlin(Spring Boot)のソースコードを自動生成する方法を紹介します。
以下の構成にしたがって進めていきます。

  • 「OpenAPI Generator について」
  • 「Kotlin(Spring Boot)に導入」
  • 「ソースコードの自動生成」
  • 「REST API の作成」
  • 「まとめ」

本記事で作成したサンプルコードは以下に載せています。

https://github.com/Msksgm/openapi-generator-sample

OpenAPI Generator について

OpenAPI Generator とは、OpenAPI Specification(以下、OpenAPI)をもとに、ソースコードを自動生成するツールです。
OpenAPI Generator を利用することで、スキーマ駆動開発を実現しやすくなります。

https://github.com/OpenAPITools/openapi-generator

OpenAPI ベースでソースコードを生成できるため、以下のメリットがあります。

  • フロントエンドとバックエンド間でAPI スキーマの認識を一致させながら並行して開発可能
  • API の設計書とソースーコードが乖離しない
  • API のコントローラ部分が自動生成されるので、開発者はドメインロジックに注力可能

OpenAPI と OpenAPI Generator についてほかにも特徴がありますが、以下の記事を参考にしたので、詳細なことは以下を参考にしてください。

https://qiita.com/amuyikam/items/e8a45daae59c68be0fc8

また、記事に引用されていた以下の資料は、OpenAPI Generator の Contributer の方が作成されたものです。
OpenAPI の歴史と OpenAPI Generator が制作された経緯を紹介しており、非常に興味深かったのでぜひ一読してください。

https://speakerdeck.com/akihito_nakano/gunmaweb34

Kotlin(Spring Boot)に導入

Kotlin に導入する際には、build.gradle.kts 記述が必要なため本項目で解説します。
Kotlin で導入すると、Controller に使用する interface と data class が生成されます。interface はエンドポイントのルーティング、メソッド、ステータスコードなどが定義されます。同様に、data class はリクエストとレスポンスの型が定義されています。

openapi.generator プラグインを導入するほかに、前提として以下のように設定します。

概要 内容
生成される interface のエンドポイントのまとめ方は、OpenAPI の tags 準拠 デフォルトだと API のパスをパースするため
生成されたソースコードは、Git で管理しない build/ 配下に生成して、builld.gradle.kts の設定で import できるようにする
compile するたびにソースコードを自動生成する 意図しない変更があったときに、ビルドを停止するため

build.gradle.kts

具体的に build.gradle.kts に何を記述するのか解説します。
長くなったので、項目(plugins、dependencies、task、そのた)ごとに分割しました。

plugins

まず、OpenAPI Generator が提供している、openapi.generator を記述します。

build.gradle.kts
// 略
plugins {
	// 中略
	/**
	 * openapi.generator
	 */
	id("org.openapi.generator") version "6.2.0"
}

dependencies

生成したソースコード内で利用されているライブラリを記述します。
src/から参照したり、ビルド時にも参照するため、記述しないとビルドが落ちます。

build.gradle.kts
dependencies {
	// 中略
	/**
	 * Swagger Annotations
	 * Swagger Models
	 * Jakarta Annotations API
	 */
	compileOnly("io.swagger.core.v3:swagger-annotations:2.2.4")
	compileOnly("io.swagger.core.v3:swagger-models:2.2.4")
	compileOnly("jakarta.annotation:jakarta.annotation-api:2.1.1")

	/**
	 * Spring Boot Starter Validation
	 */
	implementation("org.springframework.boot:spring-boot-starter-validation")
}

task

./gradlew に task を追加します。
追加するタスクは以下の表の通りです。

タスク 内容
generateApiDoc openapi.yaml から API スキーマのドキュメントを生成する
generateApiServer openapi.yaml からソースコードを生成する(tags 準拠で生成できるようにする)

また、task の追加設定で、コンパイル時にgenerateApiServerの実行するように設定します。
task<GenerateTask>("generateApiServer")に記述してある、apiPackage.setmodlePackage.setのパスは各自のパスを指定してください。main/kotlin からパスを指定します。

build.gradle.kts
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask // GenerateTask のオプションのために、import が必要。あらかじめ、build して依存ファイルがないと import できない

// 略

/**
 * OpenAPI Generator を使って API ドキュメント生成
 */
task<GenerateTask>("generateApiDoc") {
	generatorName.set("html2")
	inputSpec.set("$projectDir/openapi.yaml")
	outputDir.set("$buildDir/openapi/doc/")
}

/**
 * OpenAPI Generator を使ってコード生成
 */
task<GenerateTask>("generateApiServer") {
	generatorName.set("kotlin-spring")
	inputSpec.set("$projectDir/openapi.yaml")
	outputDir.set("$buildDir/openapi/server-code/") // .gitignoreされているので注意(わざとここにあります)
	apiPackage.set("com.example.yourapp.openapi.generated.controller") // 各自のアプリケーションに合わせてパス名を変更する
	modelPackage.set("com.example.yourapp.openapi.generated.model") // 各自のアプリケーションに合わせてパス名を変更する
	configOptions.set(
		mapOf(
			"interfaceOnly" to "true",
		)
	)
	/**
	 * true にすると tags 準拠で、API の interface を生成する
	 */
	additionalProperties.set(
		mapOf(
			"useTags" to "true"
		)
	)
}

/**
 * Kotlinをコンパイルする前に、generateApiServerタスクを実行
 * 必ずスキーマファイルから最新のコードが生成され、もし変更があったらコンパイル時に失敗して気付けるため
 */
tasks.compileKotlin {
	dependsOn("generateApiServer")
}

その他

build されたソースコードを import できるようにする設定も、build.gradle.kts に記述します。

build.gradle.kts
/**
 * OpenAPI Generator によって生成されたコードを import できるようにする
 */
kotlin.sourceSets.main {
	kotlin.srcDir("$buildDir/openapi/server-code/src/main")
}

以上で導入完了です。

ソースコードの自動生成

導入したので、どのようにして自動生成するのか確認します。

OpenAPI の作成

ソースコードの生成もとになる、OpenAPI を作成します。本記事では v3.0.2 で作成しました。
メタ情報(openapi、info、servers)、エンドポイント(paths)、コンポーネント(components)から構成されます。
具体的な記述方法について触れませんので、公式ドキュメントを参照してください。

https://spec.openapis.org/oas/v3.0.2

本記事で作成した OpenAPI。
openapi.yaml
# OpenAPI Object: openapi
#
# type
# - string
# Required
# - true
#
openapi: "3.0.2"

#
# OpenAPI Object: Info
#
# type
# - InfoObject
# Required
# - true
#
info:
  title: OpenAPI Generator Kotlin Sample
  description: OpenAPI Generator を用いた自動生成のサンプルコードです
  license:
    name: MIT License
    url: https://opensource.org/licenses/MIT
  version: "1.0"

#
# OpenAPI Object: servers
#
# type
# - [Server Object]
# Required
# - false
#
servers:
  - url: /api

#
# OpenAPI Object: paths
#
# type
# - Paths Object
# Required
# - true
#
paths:

  #
  # Paths Object
  #
  # 概要
  # - エンドポイントへの相対パスを記述する
  # - フィールド名はスラッシュから始まる
  #
  /customers:

    #
    # Method: get/put/post/delete/options/head/patch/trace
    #
    get:

      #
      # Paths Object: tags
      #
      # type
      # - [string]
      # Required
      # - false
      #
      tags:
        - Customer

      #
      # Paths Object: Summary
      #
      # type
      # - string
      # Required
      # - false
      #
      summary: カスタマーの一覧取得

      #
      # Paths Object: description
      #
      # type
      # - string
      # Required
      # - false
      #
      description: DB に登録されているカスタマーを全て取得する

      #
      # Paths Object: operationId
      #
      # type
      # - string
      # Required
      # - false
      #
      operationId: List

      #
      # Paths Object: responses
      #
      # type
      # - Responses Object
      # Required
      # - true
      #
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MultipleCustomersResponse'

#
# OpenAPI Object: components
#
# type
# - Component Object
# Required
# - No
#
components:

  #
  # Components Object: schemas
  #
  # type
  # - Map[string, Schema Object | Reference Object]
  #   - Key
  #     - String
  #   - Value
  #     - Schema Object | Reference Object
  # Required
  # - false
  #
  schemas:

    #
    # Schema Object
    #
    # 入力データ型の定義で利用
    # JSON Schema の拡張サブセット
    # 基本は JSON Schema に準拠
    #
    Customer:

      #
      # Schema Object: required
      #
      # type
      # - [string]
      # Required
      # - false
      #
      required:
        - first_name
        - last_name

      #
      # Schema Object: type
      #
      # type
      # - string
      # - | null
      # - | boolean
      # - | object
      # - | array
      # - | number
      # - | string
      #
      # Required
      # - true
      #
      type: object

      properties:
        first_name:
          type: string
        last_name:
          type: string
    MultipleCustomersResponse:
      required:
        - customers
      type: object
      properties:
        customers:
          type: array
          items:
            $ref: '#/components/schemas/Customer'

コマンドの実行

以下のコマンド(または、intellij の gradle タブ->other->generateApiServer)でソースコードを自動生成できます。
生成されたソースコードは、build/openapi/server-code/src 配下に存在します。

./gradlew generateApiServer

生成されたソースコード
生成されたソースコード

中身を確認すると、以下になっています。
OpenAPI がソースコードに反映されたことがわかりました。

CustomerApi.kt
/**
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.2.0).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/

// 中略

@Validated
@RequestMapping("\${api.base-path:/api}")
interface CustomerApi {

    @Operation(
        summary = "カスタマーの一覧取得",
        operationId = "list",
        description = "DB に登録されているカスタマーを全て取得する",
        responses = [
            ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = MultipleCustomersResponse::class))])
        ]
    )
    @RequestMapping(
            method = [RequestMethod.GET],
            value = ["/customers"],
            produces = ["application/json"]
    )
    fun list(): ResponseEntity<MultipleCustomersResponse> {
        return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
    }
}

また、以下のコマンドによって、API ドキュメントの生成も可能です。

./gradlew generateApiDoc
生成された API ドキュメント ./build/openapi/doc/index.html 。

生成されたAPIドキュメント
生成された API ドキュメント

REST API の作成

生成した interface を実装することで、REST API を作成します。

interface の実装

生成した interface の実装方法は以下になります。
build.gradle.kts の設定によりbuild/配下のファイルを import 可能なことがわかります。
非常に簡潔な実装になり、開発者が実装する部分はほとんどないことがわかります。

CustomerController.kt
package com.example.openapigeneratorsample.controller

import com.example.realworldkotlinspringbootjdbc.openapi.generated.controller.CustomerApi // 自動生成されたソースコード
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.Customer // 自動生成されたソースコード
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.MultipleCustomersResponse // 自動生成されたソースコード
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RestController

@RestController
class CustomerController : CustomerApi {
    override fun list(): ResponseEntity<MultipleCustomersResponse> {
        // 本来なら DB から値を取得する処理がある
        // 今回は簡略
        return ResponseEntity(
            MultipleCustomersResponse(
                customers = listOf(Customer("Alice", "example01"), Customer("Bob", "example02")),
            ),
            HttpStatus.OK
        )
    }
}

動作確認

一応動作確認します。

./gradlew bootRun

以下のコマンドでレスポンスが戻ってくることが確認できたら完了です。

curl -X GET \
 -H "Accept: application/json" \
 "http://localhost:8080/api/customers"
# {"customers":[{"first_name":"Alice","last_name":"example01"},{"first_name":"Bob","last_name":"example02"}]}

まとめ

本記事では OpenAPI Generator を利用して、Kotlin(Spring Boot)で API のソースコードを自動生成する方法を紹介しました。
OpenAPI Generator によってスキーマ駆動開発による並行開発や開発者が注力する領域をドメインに限定できます。
さまざまな言語で利用できるため、新規開発で新しい言語を導入する敷居も下がるので、ぜひ利用してみてください。

参考

https://blog.onk.ninja/2017/09/21/schema_first_development

https://qiita.com/amuyikam/items/e8a45daae59c68be0fc8

https://speakerdeck.com/akihito_nakano/gunmaweb34

https://ackintosh.github.io/blog/2018/05/12/openapi-generator/

Discussion