Kotlin + Spring Boot で OpenAPI Generator 体験記①（設定内容編）

OpenAPI Generator を使ってみたので、使用感やカスタマイズなどを紹介します

## 環境

* OpneAPI Generator 7.1.0
* OpenAPI Specification 3.0.3
* Spring Boot 3.1.5
* Amazon Corretto 17
* Kotlin 1.9.10

## OpenAPI Generator とは

> OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (both 2.0 and 3.0 are supported). Currently, the following languages/frameworks are supported:

引用元: https://github.com/OpenAPITools/openapi-generator#overview

一言でいうと、OpenAPI仕様からAPIクライアント、スタブサーバー、ドキュメントなどを自動生成してくれるツールです

## OpenAPI Generator 利用背景

* フロントエンドとバックエンドを別チームで分業開発するため、OpenAPIを書いてSchema駆動で開発
* せっかくOpenAPIを書くなら自動生成することで、実装時間の短縮やドキュメントと実装の仕様一致などの恩恵を受けたい

## build.gradle.kts の設定値

まずは build.gradle.kts の全体像を紹介します  
最初は最低限の設定で生成してみましたが、すぐに扱いづらい部分が見えてきてどんどん設定を足していった結果これだけ肥大化していきました。。。

なお、設定できる項目は以下で確認できます（めちゃくちゃ多い）

https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator-gradle-plugin

```kotlin
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask

plugins {
    id("org.hidetake.swagger.generator") version 7.1.0
}

tasks.withType<GenerateTask> {
    doFirst {
        delete("$projectDir/src/main/kotlin/com/example/model")
    }
    generatorName.set("kotlin-spring")
    inputSpec.set("$apiDocumentDir/openapi.yaml")
    outputDir.set("$projectDir")
    packageName.set("com.example")
    modelPackage.set("com.example.model")
    modelNameSuffix.set("Model")
    templateDir.set("$apiDocumentDir/templates")
    typeMappings.set(
        mapOf("DateTime" to "LocalDateTime")
    )
    importMappings.set(
        mapOf("LocalDateTime" to "java.time.LocalDateTime")
    )
    configOptions.set(
        mapOf(
            "annotationLibrary" to "none",
            "documentationProvider" to "none",
            "exceptionHandler" to "false",
            "gradleBuildFile" to "false",
            "interfaceOnly" to "true",
            "serializationLibrary" to "jackson",
            "useBeanValidation" to "true",
            "useSpringBoot3" to "true",
        )
    )
    ignoreFileOverride.set("$apiDocumentDir/.openapi-generator-ignore")
    doLast {
        delete("$projectDir/.openapi-generator")
    }
}
```

以下で目的別に設定を解説します

### 1.使用する Generator の設定

```kotlin
    generatorName.set("kotlin-spring")
```

タイトルの通り Kotlin + Spring Boot で使用するため、`kotlin-spring`の設定で生成してもらいます  
他言語のメジャーなものだと`php-laravel`や`ruby-on-rails`などの設定も存在

https://openapi-generator.tech/docs/generators

### 2.生成元／生成先の設定

```kotlin
    inputSpec.set("$apiDocumentDir/openapi.yaml")
    outputDir.set("$projectDir")
    packageName.set("com.example")
    modelPackage.set("com.example.model")
    modelNameSuffix.set("Model")
```

```kotlin
    doFirst {
        delete("$projectDir/src/main/kotlin/com/example/model")
    }
```

inputSpecで生成元となるOpenAPIを指定します。後の設定は出力先のディレクトリやパッケージの指定です。今回は意図せぬ差分を検知できるように生成コードもGit管理に含めており、自分達が実装したクラスと区別しやすいようにパッケージを分けたりクラス名に固定で`Model`サフィックスをつけるような生成にしています。（Git管理せずbuild以下に出力するのもあり）

若干イケてないですが、Component名を変更して再生成すると元のComponent名で生成されたクラスが残ってしまうため、前処理で全削除して洗い替えされるようにしています。

### 3.生成コードのカスタマイズ設定

```kotlin
    templateDir.set("$apiDocumentDir/templates")
    typeMappings.set(
        mapOf("DateTime" to "LocalDateTime")
    )
    importMappings.set(
        mapOf("LocalDateTime" to "java.time.LocalDateTime")
    )
    configOptions.set(
        mapOf(
            "annotationLibrary" to "none",
            "documentationProvider" to "none",
            "exceptionHandler" to "false",
            "gradleBuildFile" to "false",
            "interfaceOnly" to "true",
            "serializationLibrary" to "jackson",
            "useBeanValidation" to "true",
            "useSpringBoot3" to "true",
        )
    )
```

自動生成には`Mustacheテンプレート`が使用されているため、自前でテンプレートを用意すれば出力内容を細かくカスタマイズできます  
基本的にはデフォルトで使用されているテンプレートをコピーしてきて必要な部分を修正していく形です（元のファイル名と同じにする必要あり）

テンプレートも結構カスタマイズしているのですが、長くなるので別章で紹介します。

https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources/kotlin-spring

OpenAPIではstring型などにformatを指定できるのですが、そのformat指定時の生成型を変更することもできます。stringのdate-timeはデフォルトだとOffsetDateTimeで生成されるのですが、今回タイムゾーンは不要だったためLocalDateTimeで出力するように差し替えています。

https://swagger.io/docs/specification/data-models/data-types/#string

https://openapi-generator.tech/docs/generators/kotlin-spring/#import-mapping

configOptionsではさらに詳細な設定ができます。今回の設定内容でのポイントを紹介すると以下になります

1. Request/Response Bodyに使うようなComponent定義のみをモデルクラスとして自動生成して欲しいため、不要な設定はoffにする
1. JSONシリアライズ／デシリアライズには`Jackson`を使用
1. requiredによる必須チェックやpatternやsizeなどの書式チェックも自動生成して欲しいため、`BeanValidation`を有効化

https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/kotlin-spring.md

### 4.生成コードの除外設定

```kotlin
    ignoreFileOverride.set("$apiDocumentDir/.openapi-generator-ignore")
```

```kotlin
    doLast {
        delete("$projectDir/.openapi-generator")
    }
```

README.mdなどはオプションで自動生成offにできなかったため、ignoreファイルで管理して生成されないようにしています。が、ディレクトリだけは作られたまま残ってしまうので後処理で削除することに。。。

## 生成クラス例

さて、ここまででGeneratorの設定を紹介（Mustacheテンプレートのカスタマイズは除く)をしましたが、実際にどんな形でモデルクラスが生成されるのかを簡単に紹介します。

以下にOpenAPIの例（抜粋)と生成されるモデルクラスを掲載

```yaml
components:
    SampleResponse:
      type: object
      description: サンプルレスポンス
      properties:
        id:
          type: integer
          format: int32
          description: ID
        code:
          type: string
          pattern: '^[a-zA-Z0-9]+'
          description: コード
        name:
          type: string
          maxLength: 10
          description: 名前
        memo:
          type: string
          description: メモ
      required:
        - id
        - code
        - name
```

```kotlin
package jp.co.sample.model

import java.util.Objects
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonValue
import jakarta.validation.constraints.DecimalMax
import jakarta.validation.constraints.DecimalMin
import jakarta.validation.constraints.Email
import jakarta.validation.constraints.Max
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.Size
import jakarta.validation.Valid

/**
 * サンプルレスポンス
 * @param id ID
 * @param code コード
 * @param name 名前
 * @param memo メモ
 */
data class SampleResponseModel(

    @field:NotNull
    @get:JsonProperty("id") val id: kotlin.Int?,

    @get:Pattern(regexp="^[a-zA-Z0-9]+")
    @field:NotNull
    @get:JsonProperty("code") val code: kotlin.String?,

    @field:NotNull
    @get:Size(max=10)
    @get:JsonProperty("name") val name: kotlin.String?,

    @get:JsonProperty("memo") val memo: kotlin.String?,
) {

}
```

requiredやpattern、maxLengthの内容もBeanValidationとして生成されており、descriptionの内容もdocとして生成されます。実際にはもっとフィールド数も多く、ObjectがネストしたComponentを定義しているのでこららが自動生成されるだけでかなり助かっています。

## （一旦の）まとめ

~~ちょっと力尽きてきた~~ 記事が長くなってきたので一旦ここで区切ろうと思います。来週あたりに続きとして「Kotlin + Spring Boot で OpenAPI Generator 体験記②（Mustacheテンプレート格闘編）」を書こうと思いますので投稿されたらそちらもみていただけると幸いです。

余談ですが、弊社去年のカレンダーの同じ日（12/2）にOpenAPI Generatorの記事が投稿されていたようです（運命感じますね）

https://qiita.com/logy0704/items/3abbf16c91b34609fa3a

（追記） 続きの記事を作成しました

https://qiita.com/chorei/items/718e020c1b99f3cbed2e