OpenAPI Generatorのtypescript-fetchで型定義を自動生成してAngularで使う
はじめに
この記事では、OpenAPI Generatorのtypescript-fetchで型定義を自動生成してAngularで使う方法を試したので紹介します。この方法は、Voicyのプロダクトにも一部導入されているので、改めて自分で試してみてまとめました。
OpenAPI Generatorのtypescript-fetch
OpenAPIからTypeScriptの型定義を自動生成するには、複数のライブラリが選択肢として挙げられますが、今回はOpenAPI Generatorのtypescript-fetchを選択しました。選択した理由は、情報量と生成される型定義の使いやすさです。
OpenAPI GeneratorにはAngular向けのtypescript-angularも提供されており、これを使用することでServiceのコードも生成できます。しかし、自動生成されたServiceのコードを使用せず、型定義に焦点を当てて生成したかったこと、typescript-fetchの方で生成されるJSONに型をつけて変換する関数の提供がなかったため、選択しませんでした。
Serviceのコードを使用せず、型定義に焦点を当てたい意図は、クライアントコードをなるべく自動生成に依存せずに、自分たちでハンドリングしたいという考えがありました。それは、既存のServiceとの互換性、自動生成されたコードにおいて型エラーやランタイムエラーが発生した際の対応を考えなくて良いこと、自動生成されたコードでは対応できないケースが存在する可能性、ライブラリの依存を減らせるなどの点を考えたからです。
OpenAPI Generatorの導入
OpenAPI Generatorの導入の紹介にあたり、例としてサンプルのOpenAPIスキーマを用意したいので、この記事ではswagger-apiがOSSとして公開しているswagger-petstoreのopenapi.yamlを使用させていただきます。このサンプルは、https://petstore3.swagger.io で実際にホストされており、触って試すことができます。
npmでの導入方法
OpenAPI Generatorはマルチプラットフォーム(Linux, Mac, Windows)で使えるように@openapitools/openapi-generator-cli
というnpm packageが提供されているのでREADMEを参考に導入しました。
npm install -D @openapitools/openapi-generator-cli
次のコマンドで型生成を実行してみます。
openapi-generator-cli generate -i ../openapi.yaml -g typescript-fetch -o ./src/app/lib/api/petstore
npmでの導入でエラー
前述のコマンドを実行すると、2023年12月現在の最新安定バージョンである7.1.0でインストールが始まるのですが、エラーになりました。
Download 7.1.0 ...
Downloaded 7.1.0
Did set selected version to 7.1.0
...
Error: Exception in thread "main" java.lang.UnsupportedClassVersionError: org/openapitools/codegen/OpenAPIGenerator has been compiled by a more recent version of the Java Runtime (class file version 55.0),
どうやら、OpenAPI GeneratorはJava 11とApache Maven 3.3.4 or greaterでビルドされており、最新安定バージョンである7.1.0はJava 11のインストールが必要なようでした。
1つ前のバージョンである6.6.0に固定すると自分の環境では解決されました。
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
- "version": "7.1.0"
+ "version": "6.6.0"
}
}
Dockerでの導入方法
npmでの導入では、Java 11を入れないと最新安定バージョンである7.1.0が使えないなどがあり、そういった環境差分がない、Dockerを使う方が一般的な導入方法なのかなと思いました。公式のDockerイメージが配布されているため、READMEを参考に次のコマンドで実行できます。
docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli generate \
-i /local/openapi.yaml \
-g typescript-fetch \
-o /local/angular-client/src/app/api/petstore
型定義をAngularで使う
ここでは、Petを追加するエンドポイントの型定義を例に実装します。該当するOpenAPIスキーマは次の感じです。
/pet:
post:
tags:
- pet
summary: Add a new pet to the store
description: Add a new pet to the store
operationId: addPet
responses:
'200':
description: Successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
'405':
description: Invalid input
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
description: Create a new pet in the store
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/Pet'
...
Pet:
x-swagger-router-model: io.swagger.petstore.model.Pet
required:
- name
- photoUrls
properties:
id:
type: integer
format: int64
example: 10
name:
type: string
example: doggie
category:
$ref: '#/components/schemas/Category'
photoUrls:
type: array
xml:
wrapped: true
items:
type: string
xml:
name: photoUrl
tags:
type: array
xml:
wrapped: true
items:
$ref: '#/components/schemas/Tag'
xml:
name: tag
status:
type: string
description: pet status in the store
enum:
- available
- pending
- sold
xml:
name: pet
type: object
...
swagger-petstoreのopenapi.yamlより
自動生成した型定義のファイル
前述のコマンドでOpenAPIスキーマから型定義を生成できました。関連箇所の生成されるファイルは次の通りです。
komura-c/openapi_playground/blob/main/angular-client/src/app/lib/api/petstore/apis/PetApi.ts
export interface AddPetRequest {
pet: Pet;
}
export interface Pet {
id?: number;
name: string;
category?: Category;
photoUrls: Array<string>;
tags?: Array<Tag>;
status?: PetStatusEnum;
}
export const PetStatusEnum = {
Available: 'available',
Pending: 'pending',
Sold: 'sold'
} as const;
export type PetStatusEnum = typeof PetStatusEnum[keyof typeof PetStatusEnum];
...
export function PetFromJSON(json: any): Pet {
return PetFromJSONTyped(json, false);
}
export function PetFromJSONTyped(json: any, ignoreDiscriminator: boolean): Pet {
if ((json === undefined) || (json === null)) {
return json;
}
return {
'id': !exists(json, 'id') ? undefined : json['id'],
'name': json['name'],
'category': !exists(json, 'category') ? undefined : CategoryFromJSON(json['category']),
'photoUrls': json['photoUrls'],
'tags': !exists(json, 'tags') ? undefined : ((json['tags'] as Array<any>).map(TagFromJSON)),
'status': !exists(json, 'status') ? undefined : json['status'],
};
}
export function PetToJSON(value?: Pet | null): any {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
return {
'id': value.id,
'name': value.name,
'category': CategoryToJSON(value.category),
'photoUrls': value.photoUrls,
'tags': value.tags === undefined ? undefined : ((value.tags as Array<any>).map(TagToJSON)),
'status': value.status,
};
}
...
PetStatusEnum
のenumがきちんとTypeScriptのUnion型が使われていることや、前述したJSONに型をつけて変換する関数、PetFromJSON
やPetToJSON
が作成されるのが良いなと感じました。
Angularで型定義を読み込んで使う
AngularのServiceを作成し、型定義を読み込んで使用します。
komura-c/openapi_playground/blob/main/angular-client/src/app/lib/api/petstore-api.service.ts
import { Injectable, inject } from '@angular/core';
import { AddPetRequest } from './petstore/apis';
import { HttpClient } from '@angular/common/http';
import { Pet, PetFromJSON, PetToJSON } from './petstore/models';
import { lastValueFrom } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class PetstoreApiService {
private hostURL: string = 'バックエンドのURL';
private http = inject(HttpClient);
addPet(addPetRequest: AddPetRequest): Promise<Pet> {
const response = this.http.post<Pet>(this.hostURL + '/pet', PetToJSON(addPetRequest.pet)).pipe(
map((res) => {
return PetFromJSON(res);
})
);
return lastValueFrom(response);
}
おわりに
OpenAPI Generatorのtypescript-fetchで型定義を自動生成してAngularで使う方法をまとめてみました。
今回は、Petを追加する簡単なエンドポイントでしたが、仕様が複雑なスキーマほど自動生成する型定義が活きてくるのかなと思います。
業務では他のメンバーがかなり実装していて、自分は導入検討には参加しただけで、実際にはまだしっかりと使っていなかったので、今回改めてまとめてみて便利だなと感じました。
ここまで記事をお読みいただきありがとうございました!
Discussion