投稿日
OpenAPI定義からREST APIソースコードの自動生成方法の紹介
もくじ
はじめに
本記事ではサービス開発案件で蓄積されたノウハウとして、OpenAPIの定義ファイルからREST APIのバックエンドソースコードを自動生成して開発を行う方法を紹介します。
背景
本開発ではバックエンドはSpring Boot、 フロントエンドはReactを用いており、SPA+REST API構成サービス開発リファレンスに則り、フロントエンド-バックエンド間のAPIの認識差の発生を防止するため、 OpenAPIでAPIを定義するようにしました。
また、本開発では
- フロントエンドとバックエンドが並行して開発を行う
- 少人数で開発開始し、後に増員予定がある
という背景があり、その中で効率的な開発、品質の担保、新規参画者への参入障壁を下げる等の観点を満たして進めるために自動生成を活用することにしました。
OpenAPIとは
OpenAPIとはREST APIの定義を記述するためのフォーマットで、OpenAPI定義ファイルではAPIに関する以下のものを記述できます。
- オペレーションとエンドポイント
- オペレーションの入出力パラメータ
- 認証方法
- 問い合わせ先、ライセンス、利用規約など
使用ツール
OpenAPI Generator
API仕様を定義したYAMLファイルからソースコードを自動生成するツールです。
OpenAPI Generatorを利用した自動生成方法
公式ドキュメントのGetting Startedと OpenAPI generatorのGitHubのREADMEを参考に次のように自動生成を行いました。
1.CLIの導入
任意のディレクトリでOpenAPI generatorのGitHubからcloneします。
git clone https://github.com/OpenAPITools/openapi-generator.git
cloneしたopenapi-generatorディレクトリに移動しコンパイル、パッケージ化します。
cd openapi-generator
mvn clean package
これによりCLIのjarファイルopenapi-generator-cli.jar
が作成されます。
2.ソースコード自動生成
1.設定ファイルの作成
自動生成の前にOpenAPI generatorの設定のため下記のような設定ファイルを任意の場所に作成します。
【config.json】
{
"interfaceOnly": "true",
"apiPackage": "hoge.piyo.generated.api",
"modelPackage": "hoge.piyo.generated.model"
}
各項目の意味は下記の通りです。
interfaceOnly | サーバファイルは作成せずスタブ実装がされたAPIインタフェースのみ作成するかどうか |
apiPackage | apiクラスのパッケージ |
modelPackage | modelクラスのパッケージ |
他の設定項目やデフォルト値に関してはドキュメントを参照してください。
2.ソースコードの自動生成
openapi-generator-cli.jar
を実行し、自動生成を行います。
java -jar [openapi-generator-cli.jarのパス] generate -i [OpenAPI定義ファイルのパス] -o [出力先パス] -g spring -c [設定ファイルのパス]
-g
オプションではgenerator name
を指定します。generator name
とは生成するフームワークや言語の種類(java, springなど)を指す識別子です。 詳しくはドキュメントを参照してください。本開発ではバックエンドにSpring Bootを採用していましたのでspring
を指定しています。
ここからは自動生成の具体的な例を挙げて説明します。
例えば次のような2APIが定義されたOpenAPI定義ファイルから自動生成を行うとします。
- 商品登録API:POST /product
- 商品詳細取得API:GET /product/{product_id}
【openapi.yaml(OpenAPI定義ファイル)】
openapi: 3.0.0
info:
title: openapi
version: '1.0'
servers:
- url: 'http://localhost:8080'
paths:
'/product':
post:
operationId: post-product
tags:
- Product
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ProductRequest'
responses:
'200':
description: OK
'/product/{product_id}':
parameters:
- schema:
type: string
name: product_id
in: path
required: true
get:
operationId: get-product-product_id
tags:
- Product
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ProductResponse'
components:
schemas:
ProductResponse:
title: ProductResponse
type: object
properties:
id:
type: string
name:
type: string
price:
type: integer
required:
- id
- name
- price
ProductRequest:
title: ProductRequest
type: object
properties:
name:
type: string
price:
type: integer
required:
- name
- price
すると-o
で指定した出力先パスに下記ソースコードファイルが出力されます。
- APIインタフェース
-
- 以下のAPIが生成されます
-
-
- GET /product/{product_id}
- POST /product
-
【ProductApi.java】
/**
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
package hoge.piyo.generated.api;
import hoge.piyo.generated.model.ProductRequest;
import hoge.piyo.generated.model.ProductResponse;
import ...
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-09-08T11:56:15.267619500+09:00[Asia/Tokyo]")
@Validated
@Tag(name = "product", description = "the product API")
public interface ProductApi {
default Optional<NativeWebRequest> getRequest() {
return Optional.empty();
}
/**
* GET /product/{product_id}
*
* @param productId (required)
* @return OK (status code 200)
*/
@Operation(
operationId = "getProductProductId",
tags = { "Product" },
responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProductResponse.class)))
}
)
@RequestMapping(
method = RequestMethod.GET,
value = "/product/{product_id}",
produces = { "application/json" }
)
default ResponseEntity<ProductResponse> getProductProductId(
@Parameter(name = "product_id", description = "", required = true, schema = @Schema(description = "")) @PathVariable("product_id") String productId
) {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "{ \"price\" : 0, \"name\" : \"name\", \"id\" : \"id\" }";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}
}
});
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
/**
* POST /product
*
* @param productRequest (optional)
* @return OK (status code 200)
*/
@Operation(
operationId = "postProduct",
tags = { "Product" },
responses = {
@ApiResponse(responseCode = "200", description = "OK")
}
)
@RequestMapping(
method = RequestMethod.POST,
value = "/product",
consumes = { "application/json" }
)
default ResponseEntity<Void> postProduct(
@Parameter(name = "ProductRequest", description = "", schema = @Schema(description = "")) @Valid @RequestBody(required = false) ProductRequest productRequest
) {
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}
- モデルクラス
@JsonProperty
でjsonにした際のマッピング、@NotNull
などでBeanバリデーションが生成されます- また、equals()、hashCode()、toString()、toIndentedString()も生成されます
【ProductResponse.java】
package hoge.piyo.generated.model;
import ...
/**
* ProductResponse
*/
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-09-08T11:56:15.267619500+09:00[Asia/Tokyo]")
public class ProductResponse {
@JsonProperty("id")
private String id;
@JsonProperty("name")
private String name;
@JsonProperty("price")
private Integer price;
public ProductResponse id(String id) {
this.id = id;
return this;
}
/**
* Get id
* @return id
*/
@NotNull
@Schema(name = "id", required = true)
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
// nameもidと同様
// priceもidと同様
@Override
public boolean equals(Object o) {
// 省略
}
@Override
public int hashCode() {
// 省略
}
@Override
public String toString() {
// 省略
}
/**
* Convert the given object to string with each line indented by 4 spaces
* (except the first line).
*/
private String toIndentedString(Object o) {
// 省略
}
}
【ProductRequest.java】
>package hoge.piyo.generated.model;
import ...
/**
* ProductRequest
*/
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-09-08T11:56:15.267619500+09:00[Asia/Tokyo]")
public class ProductRequest {
@JsonProperty("name")
private String name;
@JsonProperty("price")
private Integer price;
public ProductRequest name(String name) {
this.name = name;
return this;
}
/**
* Get name
* @return name
*/
@NotNull
@Schema(name = "name", required = true)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// priceもnameと同様
@Override
public boolean equals(Object o) {
// 省略
}
@Override
public int hashCode() {
// 省略
}
@Override
public String toString() {
// 省略
}
/**
* Convert the given object to string with each line indented by 4 spaces
* (except the first line).
*/
private String toIndentedString(Object o) {
// 省略
}
}
3.生成コードの利用
APIインタフェース
APIインタフェースをimplementした下記のようなコントローラを作成することで、APIを実装していきます。
@RestController
public class ProductController implements ProductApi {
...
}
モデルクラス
モデルクラスについて独自実装を追加したい場合は自動生成したモデルクラスを継承したサブクラスを作成し、そのサブクラスに実装を追加していきます。なぜこのような手法をとるのかというと、自動生成されたコードを直接編集すると仮に仕様に変更等があり自動生成を実施し直した場合に、手で行った編集が上書きされ失われてしまうためです。ちなみに、このように自動生成されたコードを直接編集せず、サブクラスを作成し編集するデザインパターンのことをGeneration Gapパターンと呼びます。
このようにすることで、APIインタフェースとモデルクラスのソースコードを自動生成しAPIを実装することができます。
利用して感じたこと
良かった点
コーディング時間短縮
- OpenAPI定義ファイルから人手でコントローラクラス、モデルクラスのソースコードを記述する手間が削減できた
- typoや型誤り等のヒューマンエラーを防ぐことができ、インタフェースが保障されているため、フロントエンドとの接続をスムーズに行うことができた
- OpenAPI定義ファイルに修正が生じた際にも自動生成を再実施することで素早く修正対応することができた
バリデーションの実装
OpenAPI generatorの設定項目useBeanValidation
をtrueにすることで(デフォルト値:true)、OpenAPI定義ファイルで記述したバリデーションをBeanバリデーションとして自動生成でき、フロントエンドとバックエンドで同じ内容のバリデーションを比較的楽に実装することができました。
ただし、Beanバリデーションでエラーが発生した場合はorg.springframework.validation.BindException
が、型違いでエラーが発生した場合はorg.springframework.http.converter.HttpMessageNotReadableException
が投げられるのでExceptionHandler
でエラーハンドリングを行いました。
不便だった点
APIのルートパス毎にAPIインタフェースコードがまとめられてファイル出力される
OpenAPI generatorでAPIインタフェースコードを自動生成すると、APIパスのルート毎にファイルがまとめられて出力されます。
例えば下記のようなAPIが定義されていた場合は、1つのファイルProductApi.java
に出力されます。
- 商品登録API:POST /product -> ProductApi.java
- 商品取得API:GET /product/{product_id} -> ProductApi.java
また、下記のようなAPIが定義されていた場合は、別々のファイルProductsApi.java
とShopApi.java
に出力されます。
- 商品一覧API:GET /products -> ProductsApi.java
- お店の商品一覧API:GET /shop/{shop_id}/products -> ShopApi.java
- お店登録API:POST /shop -> ShopApi.java
このように、APIインタフェースコードがAPIパスのルート毎に生成されるため、APIで扱うリソース毎にAPIをまとめたい場合は、APIパスの命名規則に制約がかかります。ソース管理のしやすさを優先した本開発では、操作する主となるリソース名がAPIパスのルートにくるような命名規則を定めました。
例
- お店の商品一覧API:GET /products/shop/{shop_id}
REST APIの開発でその他便利だったツール
OpenAPIの自動生成とは直接関係ありませんが、本開発で有用に感じたツールを簡単に紹介します。
Stoplight Studio
Stoplight StudioとはOpenAPI定義ファイルをGUIで記述できるツールであり、YAMLファイルの記述に経験の浅い私でもOpenAPI定義ファイルを作成しやすかったです。また、構文誤りなどチェックできる機能もあり便利でした。
ModelMapper
ModelMapperとはオブジェクト間のBeanマッピングライブラリです。
本開発ではAPIのRequest/Responseモデルクラスの値をドメインモデルクラスへコピーするのに用いました。これによりフィールドの値の数だけ処理を記述しなければいけないところを1行で記述でき、コーディング量の削減とコードの可読性向上ができました。
Mybatis Generator
MybatisとはJavaアプリケーションとRDSとのデータのやりとりを簡略化してくれるフレームワークであり、Mybatis Generatorはそのデータやり取りのソースコードを自動生成するツールです。
本開発ではデータアクセス層のコードの自動生成も行い、単純なCRUDのAPIであればOpenAPI generatorの自動生成と合わせ、ほとんど自動生成コードでAPIが実装でき、コーディングコストの削減ができました。
Swagger UI
Swagger UIとはOpenAPI定義ファイルからHTML形式でドキュメントを生成するツールであり、本記事のopenapi.yaml
から下記のようなドキュメントを作成できます。
このドキュメント上からAPIを実行することができ、バックエンドAPI開発時のAPIのデバッグ、動作確認にGUIのような直感的操作で確認でき便利でした。
おわりに
OpenAPI generatorを用いてOpenAPIの定義ファイルからバックエンドAPIのソースコードを自動生成する流れを記述してきました。
インタフェースとモデルクラスを作成する手間を削減できた点、またインタフェースを自動生成することでインタフェースが守られAPIの単体テスト時に初めて発覚するような人的ミスを未然に防ぐことができた点で作業効率向上に寄与してくれ非常に良かったです。今後ともAPI開発時には積極的に利用していきたいと思います。