以下の記事の続きになります
環境(再掲)
- OpneAPI Generator 7.1.0
- OpenAPI Specification 3.0.3
- Spring Boot 3.1.5
- Amazon Corretto 17
- Kotlin 1.9.10
Mustacheテンプレートのカスタマイズ
OpenAPI Generator は豊富なオプションで出力内容をカスタマイズできますが、デフォルトのMustacheテンプレート
を差し替えることでさらに細かく出力内容をカスタマイズすることができます。
今回は実際に開発で変更したテンプレートの内容とその理由を説明していきます。元のテンプレートは以下で確認できます。
なお、Mustacheの記法については解説しません。公式を含めて情報が出回っているので各自ご確認お願いします。
カスタマイズ1: 数値Enumでコンパイルが通るようにする
OpenAPIでenum
として定義したものはenum class
として出力してくれます。
この時Jacksonでマッピングできるように@JsonProperty
でどのenum値にマッピングするかを生成してくれるのですが、数値enumの場合にコンパイルエラーになってしまいます。
enum class SampleEnum(val value: kotlin.Int) {
@JsonProperty(1) _1(1), // JsonPropertyのvalueはStringのためコンパイルエラー
@JsonProperty(2) _2(2) // 同上
}
若干テンプレートのバグなのでは? 疑惑はありますが、テンプレートを修正し@JsonProperty
ではなく@JsonValue
を使用する方向で回避します。
index 572875356af..603b720b705 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/enumClass.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/enumClass.mustache
@@ -2,7 +2,7 @@
* {{{description}}}
* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
*/
-enum class {{classname}}(val value: {{dataType}}) {
+enum class {{classname}}(@JsonValue val value: {{dataType}}) {
{{#allowableValues}}{{#enumVars}}
- @JsonProperty({{{value}}}) {{&name}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
+ {{&name}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
}
enum class SampleEnum(@JsonValue val value: kotlin.Int) {
_1(1),
_2(2)
}
カスタマイズ2: 必須項目をBeanValidtionでチェックさせる
KotlinはNull安全な言語であり、型宣言で?
を定義することででNull許容型になります
val nonNull: String = null // NG
val nullable: String? = null // OK
OpenAPIでrequired = true
と定義したものは非Null型として出力してくれるのですが、リクエストがNull(またはプロパティ自体が存在しない)で送られてきた場合にJacksonのマッピングでエラーになります。このエラーをハンドリングして必須項目不足の400エラーにしても良いのですが、複数のフィールドでエラーになった場合に1つしかハンドリングができず全エラー内容をAPIのエラーとして通知できません。
// 両方Nullで送られてきた場合に、「nonNull1」「nonNull2」の両方が不足していることを通知したいが、1つしかハンドリングできない
val nonNull1: String
val nonNull2: String
私たちが扱っているAPIは1APIあたりのフィールド数も多く、1度に全てのエラーを通知できないと使い勝手が悪いです。そのためNull安全のメリットを多少犠牲することになりますが、全てのフィールドをNull許容型として出力してもらい、@NotNull
で必須チェックを行うことにしています。
class SampleClass(
@get:JsonProperty("nonNull1") val nonNull1: kotlin.String,
@get:JsonProperty("nonNull2") val nonNull2: kotlin.String,
}
index d8bb06aa166..ce04efee84c 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/dataClassReqVar.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/dataClassReqVar.mustache
@@ -1,4 +1,4 @@
{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}
@Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}
@ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}
- @get:JsonProperty("{{{baseName}}}", required = true){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInCamelCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}?{{/isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}
\ No newline at end of file
+ @get:JsonProperty("{{{baseName}}}"){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInCamelCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}?{{#defaultValue}} = {{{.}}}{{/defaultValue}}
index 996353148d6..a909ee32f4e 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/beanValidationModel.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/beanValidationModel.mustache
@@ -35,4 +35,7 @@ isLong set
Not Integer, not Long => we have a decimal value!
}}{{^isInteger}}{{^isLong}}{{#minimum}}
@get:DecimalMin("{{.}}"){{/minimum}}{{#maximum}}
- @get:DecimalMax("{{.}}"){{/maximum}}{{/isLong}}{{/isInteger}}
\ No newline at end of file
+ @get:DecimalMax("{{.}}"){{/maximum}}{{/isLong}}{{/isInteger}}{{!
+check required
+}}{{#required}}
+ @field:NotNull{{/required}}
class SampleClass(
@field:NotNull
@get:JsonProperty("nonNull1") val nonNull1: kotlin.String?,
@field:NotNull
@get:JsonProperty("nonNull2") val nonNull2: kotlin.String?,
}
カスタマイズ3: デフォルト値null
を出力させないようにする
Kotlinではメソッドやコンストラクタの引数にデフォルト値を設定することができ、OpenAPIでデフォルト値を定義しなかった場合はデフォルト値null
として出力されます。1
class SampleResponse(
@get:JsonProperty("field1") val field1: kotlin.String? = null,
}
私たちは出力されたクラスをAPIのRequest/Response定義として使用しています。このようにデフォルト値が設定されてしまうとAPI仕様変更時に実装を修正しなくてもコンパイルが通ってしまい、修正漏れに気づきづらくなるため好ましくありません。(主にResponseで使用するclass)
class SampleResponse(
@get:JsonProperty("field1") val field1: kotlin.String? = null,
@get:JsonProperty("field2") val field2: kotlin.String? = null, // 追加された項目
}
@RestController
class SampleController {
@GetMapping("/sample")
fun sample(): SampleResponse {
return SampleResponse(
field1 = "hoge",
// field2にはデフォルト値が設定されているため、使用側を修正しなくてもコンパイルが通る
)
}
}
API仕様変更時に気づきやすいようにデフォルト値の設定を削除します。
class SampleResponse(
@get:JsonProperty("field1") val field1: kotlin.String? = null,
}
index 211736d7912..286139970d3 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/dataClassOptVar.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/dataClassOptVar.mustache
@@ -2,4 +2,4 @@
@Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}
@ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}{{#deprecated}}
@Deprecated(message = ""){{/deprecated}}
- @get:JsonProperty("{{{baseName}}}"){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInCamelCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}
\ No newline at end of file
+ @get:JsonProperty("{{{baseName}}}"){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInCamelCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}?{{#defaultValue}} = {{{.}}}{{/defaultValue}}
\ No newline at end of file
class SampleResponse(
@get:JsonProperty("field1") val field1: kotlin.String?,
}
まとめ
今回は、Kotlin + Spring Boot で OpenAPI Generator を使用した際に参考になりやすそうなMustacheテンプレートの修正を3点ご紹介しました。ツールやライブラリ自体に手を入れずテンプレートの修正だけで細かく調整できるのは自由度が高く素晴らしいですね。
-
OpenAPIで
default
を指定した場合はその値を出力してくれます。ですが、私たちはnull
の場合と同様の理由でdefault
の使用を禁止しています。API仕様の明示のためにdefault
を活用した方がいいケースはありますが、今回のAPIはフロントエンドサービスからのみ呼び出されるAPIなためメリットが小さく先のデメリットとのバランスで禁止しています。 ↩