りんごとバナナとエンジニア

エンジニア修行の記録

【Spring Boot】OpenAPI GeneratorのType Mappingで自作のドメインオブジェクトを使用する

Spring Boot + OpenAPI GeneratorでTodoリストのAPIサーバを作っている。OpenAPIでエンドポイントやリクエスト・レスポンスを定義するところは楽だったが、 OpenAPIで定義したリクエスト・レスポンスの型は自動で生成されたコードの中に定義されることがわかった。そのため、Spring Boot側でドメインオブジェクトを定義しても、それをそのままリクエストやレスポンスの型として使うことができない。
自分はGeneration Gapパターンを使って開発しているので、自動生成されたコードには手を加えたくないし、gitの管理下にも入れたくない。そのためSpring Bootの側でConverterを用意するしかないかと思っていたのだが、調べてみるとOpenAPI GeneratorのType Mappingという機能で賄えそうだった。

Type Mappingとは

OpenAPIで定義した型と、実装コード側で定義した型を変換することができる機能。設定項目は以下の2つがある。

  • --type-mappings : 変換先の型を指定する。
  • --import-mappings : 変換に伴いインポートする型のテンプレートを指定する。

たいていの場合、両方を指定する必要がある。

Type Mappingを使う

今回作っているのは簡単なTodoリストのRESTful APIであるため、SpringBoot側で定義したドメインを、そのままCREATE時のレスポンスの型として使いたい。
OpenAPI側では、レスポンスを Task 型として以下のように定義した。

paths:
  /tasks:
    post:
      summary: add a new task
      operationId: addTask
      requestBody:
        description: task to create
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaskRequest'
      responses:
        '201':
          description: created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        default:
          description: unexpected error
          content: 
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    Task:
      description: one task object
      type: object
      allOf:
        - $ref: '#/components/schemas/TaskRequest'
        - required:
          - id
          - done
          properties:
            id:
              type: integer
              format: int64
            done:
              type: boolean
    TaskRequest:
      description: object to create or edit a new task
      type: object
      required:
        - content
        - urgency
        - importance
      properties:
        content:
          type: string
        urgency:
          type: integer
          format: int32
        importance:
          type: integer
          format: int32

この結果、OpenAPI Generatorによって以下のようなメソッドが生成される。 ResponseEntity<Task> が戻り値となっており、この Task 型も Task.java として生成されている。

default ResponseEntity<Task> addTask(@ApiParam(value = "task to create"  )  @Valid @RequestBody(required = false) TaskRequest taskRequest) {
...
}

一方、Spring Boot側では、以下のようなドメインオブジェクトを定義した。

@Entity
@Table(name="task")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TaskEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    private String content;
    private int urgency;
    private int importance;
    private boolean isDone;

    public TaskEntity(String content, Integer urgency, Integer importance, boolean isDone) {
        this.content = content;
        this.urgency = urgency;
        this.importance = importance;
        this.isDone = isDone;
    }
}

始めは自動生成された Task 型のみでサーバサイドの処理を行えないかと思ったが、Entityとして管理されているドメインオブジェクトを使わなければJpaをうまく使うことができない。
そこで、Type Mappingで変換をかける。 build.gradle に以下の設定を追加する。

openApiGenerate {
    typeMappings = [
            Task: 'com.example.springboottodo.TaskEntity'
    ]
    importMappings = [
            Task: 'com.example.springboottodo.TaskEntity'
    ]
{

この状態で再度OpenAPI Generatorでコードを生成してみると、 addTask メソッドの返り値の型が変わった。

default ResponseEntity<com.example.springboottodo.TaskEntity> addTask(@ApiParam(value = "task to create"  )  @Valid @RequestBody(required = false) TaskRequest taskRequest) {
...
}

これで、直接 TaskEntity 型を使ってレスポンスを作れるようになる。
なお、最初に生成された Task 型はコード内では使われなくなるが、OpenAPIからドキュメントを生成するときは Task 型に基づいた記載となる。