OpenAPI Generator(Spring Boot)でカスタムテンプレートを使用する

# 前書き
OpenAPI(swagger)を使用してWebAPI設計を行う機会があり、出力されるコードをカスタマイズしなくてはならない！となりました。
しかし、初めてOpenAPIを使用したことと、日本語で解説してくれているサイトが少なかったこともあり苦労しました。

そのため個人的な備忘録としての意味も込めて、今回は紹介させていただきます。
(初めての記事の為間違いや、わかりにくい部分があったらコメントなどで教えていただけるとありがたいです。)

# バージョン
openapi-generator-cli 7.4.0

# OpenAPI Generatorとは
OpenAPI Generatorは、OpenAPI仕様（Swaggerとしても知られる）に基づいて、APIクライアント、サーバースタブ、APIドキュメント、設定ファイルなどを自動生成するツールです。これにより、APIの開発や統合が効率的に行えるようになります。例えば、OpenAPI仕様書からJava、Python、JavaScriptなど様々なプログラミング言語向けのコードを生成できます。

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

**openapi-generator-cli-7.4.0**

https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.4.0/openapi-generator-cli-7.4.0.jar


**サンプルAPIの定義**
API定義は以下のように記述します。

```yaml:sample.yaml
openapi: 3.0.3

info:
  title: Sample API
  version: 1.0.0
  description: サンプル用のWebAPI仕様書
servers:
  - url: http://localhost/api

paths:
  /sample:
    post:
      tags:
        - Sample
      summary: Sample API
      description: Sample情報を登録します。
      requestBody:
        description: 入力内容
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SampleRequest'
        required: true
      responses:
        "201":
          description: Success
        "400":
          description: Bad Request
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    description: エラー内容
                    example: "エラー内容"
                  # 必要に応じて追加  
                  
components:
  schemas:
    SampleRequest:
      type: object
      properties:
        id:
          type: integer
          format: int32
          example: 10
          description: ID
        hoge:
          type: string
          example: "sample"
          description: hoge
```


上記のyamlファイルに対してjarで以下のように処理するとコードが生成されます。

```
java -jar openapi-generator-cli-7.4.0.jar generate ^
-i ./sample.yaml ^
-g spring ^
-o generatedFile ^
--additional-properties=^
artifactId=Sample,^
artifactVersion=1.0.0,^
documentationProvider=none,^
apiPackage=com.example.controller,^
modelPackage=com.example.model,
```

# バリデーションの実装
OpenAPIではschemasに以下のように記述することでバリデーションを実装することができます。
```yaml:sample.yaml
properties:
    id:
        type: integer
        format: int32
        minimum: 0
        maximum: 100
        example: 10
        description: ID
    hoge:
        type: string
        maxLength: 30
        example: "sample"
        description: hoge
required:
    - id
    - hoge
```

sample.yamlを上記に修正してコードを生成するとSpringの場合はmodelにアノテーションが付与されます。

```java:SampleRequest.java
  /**
   * ID
   * minimum: 0
   * maximum: 100
   * @return id
  */
  @NotNull @Min(0) @Max(100) 
  @JsonProperty("id")
  public Integer getId() {
    return id;
  }

  public void setId(Integer id) {
    this.id = id;
  }

  public SampleRequest hoge(String hoge) {
    this.hoge = hoge;
    return this;
  }

  /**
   * hoge
   * @return hoge
  */
  @NotNull @Size(max = 30) 
  @JsonProperty("hoge")
  public String getHoge() {
    return hoge;
  }

  public void setHoge(String hoge) {
    this.hoge = hoge;
  }
```

OpenAPIにはいくつかデフォルトでバリデーションが準備されています。
詳しくは以下を参照してください。

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

しかし、今回はデフォルトで生成されるバリデーションだけでは足りませんでした。
ここからが本題のカスタムテンプレートです。

# カスタムテンプレートでバリデーションを実装する

OpenAPIではrequiredを指定すると@NotNullが付与されます。
しかしStringのhogeに対しても同じく付与されてしまうので空文字の判定ができません。
Stringの場合はNotNullではなくNotEmptyを付与できるように変更することになりました。

まず、OpenAPI Generatorはコードの生成時にtemplateを使用しています。
openapi-generator-cliに対して以下を実行することでtemplateファイルを取得できます。

```
java -jar openapi-generator-cli-7.4.0.jar author template -g spring -o templates
```

`-t ./templates`を追加して取得したtemplateを使用するようにコード生成時のコマンドを変更します。

```
java -jar openapi-generator-cli-7.4.0.jar generate ^
-i ./sample.yaml ^
-g spring ^
-t ./templates ^ ←追加
-o generatedFile ^
--additional-properties=^
artifactId=Sample,^
artifactVersion=1.0.0,^
documentationProvider=none,^
apiPackage=com.example.controller,^
modelPackage=com.example.model,
```

これでデフォルトのtemplateではなく指定したtemplateを使用するようになりました。
次はtemplateの修正を行っていきます。
****
**templateの修正**
取得したtemplatesフォルダを開くとmustasheファイルがごろごろいます。
mustache記法についてはわかりやすく解説してくれている記事があったので以下を参考にしました。

https://qiita.com/sengok/items/1d958348215647a5eaf0

その中のbeanValidation.mustacheを修正します。
(見やすくするために整形しています。)

```mustache:beanValidation.mustache
{{#required}}
  {{^isReadOnly}}@NotNull {{/isReadOnly}}
{{/required}}

{{#isContainer}}
  {{^isPrimitiveType}}
    {{^isEnum}}@Valid {{/isEnum}}
  {{/isPrimitiveType}}
{{/isContainer}}

{{^isContainer}}
  {{^isPrimitiveType}}@Valid {{/isPrimitiveType}}
{{/isContainer}}

{{^openApiNullable}}{{>beanValidationCore}}{{/openApiNullable}}

{{#openApiNullable}}
  {{^useOptional}}{{>beanValidationCore}}{{/useOptional}}
{{/openApiNullable}}

{{#useOptional}}
  {{#openApiNullable}}
    {{#isContainer}}
      {{^required}}{{>beanValidationCore}}{{/required}}
    {{/isContainer}}
  {{/openApiNullable}}

  {{#openApiNullable}}
    {{#required}}{{>beanValidationCore}}{{/required}}
  {{/openApiNullable}}
{{/useOptional}}
```

上記の`{{#required}}`の部分がrequiredを指定した場合のアノテーションです。
こちらにStringの場合の処理を追加します。

```mustache:beanValidation.mustache
{{#required}}
  {{^isReadOnly}}@NotNull 
  {{^isDate}}
    {{^isDateTime}}
      {{#isString}}@NotEmpty {{/isString}}
    {{/isDateTime}}
  {{/isDate}}
{{/isReadOnly}}
{{/required}}

{{#isContainer}}
  {{^isPrimitiveType}}
    {{^isEnum}}@Valid {{/isEnum}}
  {{/isPrimitiveType}}
{{/isContainer}}

{{^isContainer}}
  {{^isPrimitiveType}}@Valid {{/isPrimitiveType}}
{{/isContainer}}

{{^openApiNullable}}{{>beanValidationCore}}{{/openApiNullable}}

{{#openApiNullable}}
  {{^useOptional}}{{>beanValidationCore}}{{/useOptional}}
{{/openApiNullable}}

{{#useOptional}}
  {{#openApiNullable}}
    {{#isContainer}}
      {{^required}}{{>beanValidationCore}}{{/required}}
    {{/isContainer}}
  {{/openApiNullable}}

  {{#openApiNullable}}
    {{#required}}{{>beanValidationCore}}{{/required}}
  {{/openApiNullable}}
{{/useOptional}}
```

これでStringの場合@NotNullが付与されるようになります。

実際に生成しなおすと以下のようになります。

```java:SampleRequest.java
  /**
   * ID
   * minimum: 0
   * maximum: 100
   * @return id
  */
  @NotNull @Min(0) @Max(100) 
  @JsonProperty("id")
  public Integer getId() {
    return id;
  }

  public void setId(Integer id) {
    this.id = id;
  }

  public SampleRequest hoge(String hoge) {
    this.hoge = hoge;
    return this;
  }

  /**
   * hoge
   * @return hoge
  */
  @NotNull @NotEmpty @Size(max = 30) 
  @JsonProperty("hoge")
  public String getHoge() {
    return hoge;
  }

  public void setHoge(String hoge) {
    this.hoge = hoge;
  }
```

hogeにのみ@NotEmptyが付与されています。

# vendorExtensionsでプロパティの追加

さらに追加で各アノテーションにmessageを設定することになりました。
手動で追加してもよかったですが、再生成時の再現性なども考えてyamlファイルで指定できるようにカスタマイズします。
messageに関しては何かしらの方法で個別の文字列を追加する必要があります。
そうなってくるとGenerator自体を修正しないと厳しいかなと思いましたが、vendorExtensionsというものがありました。

https://swagger.io/docs/specification/openapi-extensions/

`x-`から始まる独自のプロパティを設定することができます。
こちらを使用してtemplateを修正していきます。

各アノテーションが定義されているファイルは`beanValidationCore.mustache`になります。

```mustache:beanValidationCore.mustache
{{#pattern}}{{^isByteArray}}@Pattern(regexp = "{{{pattern}}}"{{#vendorExtensions.x-pattern-message}}, message="{{vendorExtensions.x-pattern-message}}"{{/vendorExtensions.x-pattern-message}}) {{/isByteArray}}{{/pattern}}{{!
minLength && maxLength set
}}{{#minLength}}{{#maxLength}}@Size(min = {{minLength}}, max = {{maxLength}}) {{/maxLength}}{{/minLength}}{{!
minLength set, maxLength not
}}{{#minLength}}{{^maxLength}}@Size(min = {{minLength}}) {{/maxLength}}{{/minLength}}{{!
minLength not set, maxLength set
}}{{^minLength}}{{#maxLength}}@Size(max = {{.}}) {{/maxLength}}{{/minLength}}{{!
@Size: minItems && maxItems set
}}{{#minItems}}{{#maxItems}}@Size(min = {{minItems}}, max = {{maxItems}}) {{/maxItems}}{{/minItems}}{{!
@Size: minItems set, maxItems not
}}{{#minItems}}{{^maxItems}}@Size(min = {{minItems}}) {{/maxItems}}{{/minItems}}{{!
@Size: minItems not set && maxItems set
}}{{^minItems}}{{#maxItems}}@Size(max = {{.}}) {{/maxItems}}{{/minItems}}{{!
@Email: useBeanValidation
}}{{#isEmail}}{{#useBeanValidation}}@{{javaxPackage}}.validation.constraints.Email {{/useBeanValidation}}{{!
@Email: performBeanValidation exclusive
}}{{^useBeanValidation}}{{#performBeanValidation}}@org.hibernate.validator.constraints.Email {{/performBeanValidation}}{{/useBeanValidation}}{{/isEmail}}{{!
check for integer or long / all others=decimal type with @Decimal*
isInteger set
}}{{#isInteger}}{{#minimum}}@Min({{.}}) {{/minimum}}{{#maximum}}@Max({{.}}) {{/maximum}}{{/isInteger}}{{!
isLong set
}}{{#isLong}}{{#minimum}}@Min({{.}}L) {{/minimum}}{{#maximum}}@Max({{.}}L) {{/maximum}}{{/isLong}}{{!
Not Integer, not Long => we have a decimal value!
}}{{^isInteger}}{{^isLong}}{{#minimum}}@DecimalMin({{#exclusiveMinimum}}value = {{/exclusiveMinimum}}"{{minimum}}"{{#exclusiveMinimum}}, inclusive = false{{/exclusiveMinimum}}) {{/minimum}}{{#maximum}}@DecimalMax({{#exclusiveMaximum}}value = {{/exclusiveMaximum}}"{{maximum}}"{{#exclusiveMaximum}}, inclusive = false{{/exclusiveMaximum}}) {{/maximum}}{{/isLong}}{{/isInteger}}
```

template内では`vendorExtensions.x-hogehoge`のように記述します
正規表現に関してはデフォルトの状態で`vendorExtensions.x-pattern-message`が設定されているので、これを真似して`vendorExtensions.x-message`を追加していきます。

```mustache:
{{#minimum}}@Min({{.}}) {{/minimum}}{{#maximum}}@Max({{.}}) {{/maximum}}
```
これを以下のように変更します。
```mustache:
{{#minimum}}@Min({{.}} {{#vendorExtensions.x-message}}, message="{{vendorExtensions.x-message}}"{{/vendorExtensions.x-message}}) {{/minimum}}{{#maximum}}@Max({{.}} {{#vendorExtensions.x-message}}, message="{{vendorExtensions.x-message}}"{{/vendorExtensions.x-message}}) {{/maximum}}
```

schemasのidに`x-message`を付与してコードを生成してみます。
schemasでは`x-hogehoge`のように記述し、`vendorExtensions`はつけません。

```yaml:sample.yaml
properties:
    id:
        type: integer
        format: int32
        minimum: 0
        maximum: 100
        x-message: 特殊なエラーメッセージ
        example: 10
        description: ID
```

```java:SampleRequest.java
  /**
   * ID
   * minimum: 0
   * maximum: 100
   * @return id
  */
  @NotNull @Min(0 , message="特殊なエラーメッセージ") @Max(100 , message="特殊なエラーメッセージ") 
  @JsonProperty("id")
  public Integer getId() {
    return id;
  }

  public void setId(Integer id) {
    this.id = id;
  }

  public SampleRequest hoge(String hoge) {
    this.hoge = hoge;
    return this;
  }
```

messageが追加されました。

# 最後に

今回はカスタムテンプレートを使用してカスタマイズする方法を紹介させていただきました。カスタムテンプレートだけでなくGenerator自体を修正すれば生成されるコードはかなり自由度が高いと思います。


カスタムテンプレートの紹介だけでしたが、Generator自体にも修正は加えたので機会があればそちらについても紹介させていただきたいなと思います。