⚙️

OpenAPI Generatorのtypescript-fetchで型定義を自動生成してAngularで使う

2023/12/22に公開

はじめに

この記事では、OpenAPI Generatorのtypescript-fetchで型定義を自動生成してAngularで使う方法を試したので紹介します。この方法は、Voicyのプロダクトにも一部導入されているので、改めて自分で試してみてまとめました。

OpenAPI Generatorのtypescript-fetch

OpenAPIからTypeScriptの型定義を自動生成するには、複数のライブラリが選択肢として挙げられますが、今回はOpenAPI Generatortypescript-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に固定すると自分の環境では解決されました。

openapi.tools.json
{
  "$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スキーマは次の感じです。

openapi.yaml
 /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

PetApi.ts
export interface AddPetRequest {
    pet: Pet;
}
Pet.ts
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に型をつけて変換する関数、PetFromJSONPetToJSONが作成されるのが良いなと感じました。

Angularで型定義を読み込んで使う

AngularのServiceを作成し、型定義を読み込んで使用します。
komura-c/openapi_playground/blob/main/angular-client/src/app/lib/api/petstore-api.service.ts

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を追加する簡単なエンドポイントでしたが、仕様が複雑なスキーマほど自動生成する型定義が活きてくるのかなと思います。
業務では他のメンバーがかなり実装していて、自分は導入検討には参加しただけで、実際にはまだしっかりと使っていなかったので、今回改めてまとめてみて便利だなと感じました。

ここまで記事をお読みいただきありがとうございました!

Voicyテックブログ

Discussion