こんにちは。 エキサイト株式会社の三浦です。
以前、Spring BootからOpenAPIのドキュメントを自動生成する方法について説明しました。
今回はその中で少し詰まった、「同名のクラスを components.schemas
として登録する方法」について説明します。
components.schemaとは
components.schemas
は、OpenAPIの仕様の一つで、使い回しが出来るよう纏められたモデルのドキュメントです。
Often, multiple API operations have some common parameters or return the same response structure. To avoid code duplication, you can place the common definitions in the global components section and reference them using $ref.
例えば、Spring Bootで以下のようなコードからOpenAPIドキュメントを自動生成してみます。
@RestController @RequestMapping("sample") public class SampleController { @GetMapping public SampleResponseModel sample() { SampleResponseModel responseModel = new SampleResponseModel(); responseModel.setValue("Hello world!"); return responseModel; } @Data static class SampleResponseModel { private String value; } }
すると、以下のようなYamlファイルが出力されます。
openapi: 3.0.1 info: title: OpenAPI definition version: v0 servers: - url: http://localhost description: Generated server url paths: /sample: get: tags: - sample-controller operationId: sample responses: "200": description: OK content: '*/*': schema: $ref: '#/components/schemas/SampleResponseModel' components: schemas: SampleResponseModel: type: object properties: value: type: string
この2つを比較すると、以下のことがわかります。
- Spring Bootコード側でレスポンスモデルとして定義していた
SampleResponseModel
が、OpenAPIドキュメントの中ではcomponents.schemas
として定義されている /sample
エンドポイントのレスポンスの型が、直接指定されているのではなく、$ref: '#/components/schemas/SampleResponseModel'
のようにcomponents.schemas
を指すようになっている
こうすることで、 SampleResponseModel
をOpenAPIドキュメント内で使い回すことが出来るようになります。
例えば、以下のようなSpring Bootコードを考えてみます。
@RestController @RequestMapping("sample") public class SampleController { @GetMapping public SampleResponseModel sample() { SampleResponseModel responseModel = new SampleResponseModel(); responseModel.setValue("Hello world!"); return responseModel; } // 同じモデルを返すエンドポイントを追加 @GetMapping("2") public SampleResponseModel sample2() { SampleResponseModel responseModel = new SampleResponseModel(); responseModel.setValue("Hello world!"); return responseModel; } @Data static class SampleResponseModel { private String value; } }
同じモデルを返すエンドポイントを追加してみました。
すると、OpenAPIドキュメントは以下のようになります。
openapi: 3.0.1 title: OpenAPI definition version: v0 servers: - url: http://localhost description: Generated server url paths: /sample: get: tags: - sample-controller operationId: sample responses: "200": description: OK content: '*/*': schema: $ref: '#/components/schemas/SampleResponseModel' /sample/2: get: tags: - sample-controller operationId: sample2 responses: "200": description: OK content: '*/*': schema: $ref: '#/components/schemas/SampleResponseModel' components: schemas: SampleResponseModel: type: object properties: value: type: string
SampleResponseModel
は二重に定義されず、 components.schemas
として定義された SampleResponseModel
が使い回される形になっているのがわかります。
このように、 components.schemas
はドキュメントの可読性や文量を減らすためにとても有用な機能なのですが、Spring Boot上で自動生成する場合に問題が起きる場合があります。
その問題とは、「同じ名前の components.schemas
を登録できないこと」です。
同じ名前のcomponents.schemasを登録できない
以下のようなコードを考えてみます。
@RestController @RequestMapping("sample") public class SampleController { @GetMapping public SampleResponseModel sample() { SampleResponseModel responseModel = new SampleResponseModel(); responseModel.setValue("Hello world!"); return responseModel; } @Data static class SampleResponseModel { private String value; } }
@RestController @RequestMapping("sample2") public class Sample2Controller { @GetMapping public SampleResponseModel sample() { SampleResponseModel responseModel = new SampleResponseModel(); responseModel.setValue2("Hello world!"); return responseModel; } @Data static class SampleResponseModel { // 2 の方は、フィールド名を変更 private String value2; } }
先程のエンドポイントを、メソッドではなくクラスごと分けてみました。
また、レスポンスモデルについて、名前は同じですがフィールド名を変えています。
これでOpenAPIドキュメントを自動生成すると、以下のようになります。
openapi: 3.0.1 title: OpenAPI definition version: v0 servers: - url: http://localhost description: Generated server url paths: /sample: get: tags: - sample-controller operationId: sample responses: "200": description: OK content: '*/*': schema: $ref: '#/components/schemas/SampleResponseModel' /sample2: get: tags: - sample-2-controller operationId: sample_1 responses: "200": description: OK content: '*/*': schema: $ref: '#/components/schemas/SampleResponseModel' components: schemas: SampleResponseModel: type: object properties: value: type: string
なんと components.schemas
には、 value2
というフィールドを持つモデルが登録されていません!
というのも、 components.schemas
には同名のモデルを登録することはできないのです。
そのため、 value
をフィールドに持つモデルのみが残り、 value2
の方は消えてしまったのでした。
ですがもちろん、ドキュメントとしてはこれは不完全です。 では、どうすれば良いのでしょうか?
同名のクラスをcomponents.schemasとして登録する方法
解決方法は2つあります。
モデル名が被らないようにする
1つ目は、そもそもSpring Boot側でモデル名が被らないようにすることです。
先程の value2
の方のコードを以下のように変えてみます。
@RestController @RequestMapping("sample2") public class Sample2Controller { @GetMapping public SampleResponseModel2 sample() { SampleResponseModel2 responseModel = new SampleResponseModel2(); responseModel.setValue2("Hello world!"); return responseModel; } @Data static class SampleResponseModel2 { // 名前を被らないように変更 private String value2; } }
すると、OpenAPIドキュメントは以下のようになります。
openapi: 3.0.1 title: OpenAPI definition version: v0 servers: - url: http://localhost description: Generated server url paths: /sample: get: tags: - sample-controller operationId: sample responses: "200": description: OK content: '*/*': schema: $ref: '#/components/schemas/SampleResponseModel' /sample2: get: tags: - sample-2-controller operationId: sample_1 responses: "200": description: OK content: '*/*': schema: $ref: '#/components/schemas/SampleResponseModel2' components: schemas: SampleResponseModel: type: object properties: value: type: string SampleResponseModel2: type: object properties: value2: type: string
value2
をフィールドに持つ SampleResponseModel2
が生成され、 /sample2
エンドポイントの返り値の型として使われているのがわかります。
これで、無事想定通りのドキュメントになりました。
ただ、この方法で解決できるならいいのですが、Spring Boot側のコードのアーキテクチャやルール的に、同名のレスポンスモデルを作りたいという場合もあるでしょう。
その場合は、次の方法で解決できます。
自分でcomponents.schemasの名前をつける
デフォルトでは components.schemas
への登録名はモデル名になりますが、アノテーションをつければ手動でも設定できます。
以下のようにします。
@RestController @RequestMapping("sample") public class SampleController { @GetMapping public SampleResponseModel sample() { SampleResponseModel responseModel = new SampleResponseModel(); responseModel.setValue("Hello world!"); return responseModel; } @Data @Schema(name = "SampleResponseModel") // Schemaアノテーションを追加 static class SampleResponseModel { private String value; } }
@RestController @RequestMapping("sample2") public class Sample2Controller { @GetMapping public SampleResponseModel sample() { SampleResponseModel responseModel = new SampleResponseModel(); responseModel.setValue2("Hello world!"); return responseModel; } @Data @Schema(name = "SampleResponseModel2") // Schemaアノテーションを追加 static class SampleResponseModel { private String value2; } }
このように、レスポンスモデルに @Schema
というアノテーションを、 name
付きで追加します。
すると、OpenAPIドキュメントは以下のようになります。
openapi: 3.0.1 title: OpenAPI definition version: v0 servers: - url: http://localhost description: Generated server url paths: /sample: get: tags: - sample-controller operationId: sample responses: "200": description: OK content: '*/*': schema: $ref: '#/components/schemas/SampleResponseModel' /sample2: get: tags: - sample-2-controller operationId: sample_1 responses: "200": description: OK content: '*/*': schema: $ref: '#/components/schemas/SampleResponseModel2' components: schemas: SampleResponseModel: type: object properties: value: type: string SampleResponseModel2: type: object properties: value2: type: string
この方法により、モデル名ではなく name
で指定した名前で登録できることがわかります!
つまり、コーディングルール的にレスポンスモデル名を同じにする必要がある場合でも、この方法は使用可能ということになります。
また、今回はサンプルのため SampleResponseModel2
としましたが、たとえばパッケージ名を含めるなどすれば、より適切な名前になるでしょう。
最後に
今回の「 components.schemas
に同名のクラスは登録できない」のように、OpenAPIドキュメントを自動生成することになると、通常の開発に加えてOpenAPI側の仕様も考える必要が出てきます。
一見面倒が増えるだけに見えますが、それでも中長期的に考えれば、APIドキュメントが自動生成されるのは保守性や開発容易性のために大きな貢献となるはずです。
ぜひ皆さんも使ってみてください。