前書き
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定義は以下のように記述します。
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に以下のように記述することでバリデーションを実装することができます。
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にアノテーションが付与されます。
/**
* 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を修正します。
(見やすくするために整形しています。)
{{#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の場合の処理を追加します。
{{#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が付与されるようになります。
実際に生成しなおすと以下のようになります。
/**
* 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
になります。
{{#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
はつけません。
properties:
id:
type: integer
format: int32
minimum: 0
maximum: 100
x-message: 特殊なエラーメッセージ
example: 10
description: ID
/**
* 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自体にも修正は加えたので機会があればそちらについても紹介させていただきたいなと思います。