0
0

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

Posted at

前書き

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

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

バージョン

openapi-generator-cli 7.4.0

OpenAPI Generatorとは

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

openapi-generator-cli-7.4.0

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

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に以下のように記述することでバリデーションを実装することができます。

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にアノテーションが付与されます。

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にはいくつかデフォルトでバリデーションが準備されています。
詳しくは以下を参照してください。

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

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

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記法についてはわかりやすく解説してくれている記事があったので以下を参考にしました。

その中のbeanValidation.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の場合の処理を追加します。

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が付与されるようになります。

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

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というものがありました。

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

各アノテーションが定義されているファイルはbeanValidationCore.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を追加していきます。

{{#minimum}}@Min({{.}}) {{/minimum}}{{#maximum}}@Max({{.}}) {{/maximum}}

これを以下のように変更します。

{{#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はつけません。

sample.yaml
properties:
    id:
        type: integer
        format: int32
        minimum: 0
        maximum: 100
        x-message: 特殊なエラーメッセージ
        example: 10
        description: ID
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自体にも修正は加えたので機会があればそちらについても紹介させていただきたいなと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0