Spring Bootで自動生成するOpenAPIで、同名のクラスをcomponents.schemasとして登録する方法

こんにちは。 エキサイト株式会社の三浦です。

以前、Spring BootからOpenAPIのドキュメントを自動生成する方法について説明しました。

tech.excite.co.jp

今回はその中で少し詰まった、「同名のクラスを components.schemas として登録する方法」について説明します。

components.schemaとは

components.schemas は、OpenAPIの仕様の一つで、使い回しが出来るよう纏められたモデルのドキュメントです。

swagger.io

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ドキュメントが自動生成されるのは保守性や開発容易性のために大きな貢献となるはずです。

ぜひ皆さんも使ってみてください。