• トップ
  • ブログ一覧
  • 【Swagger】SwaggerCodegen のコード生成をカスタマイズする
  • 【Swagger】SwaggerCodegen のコード生成をカスタマイズする

    りゅうちゃん(エンジニア)りゅうちゃん(エンジニア)
    2021.12.10

    IT技術

    はじめに

    今回は SwaggerCodegen を使ったdartのコード生成を色々カスタマイズしていきます。

    dart言語を扱っていますが、ほとんど dart の知識は必要のない記事になっていますので、他の言語の場合でも参考になるかと思います!

    SwaggerCodegen のバージョンはv3を使用します。

    Swaggerについて

    Swagger は RESTful API を設計するためのオープンソースの開発者ツールです。

    SwaggerCodegen では Swagger で書かれた API 定義書から、API クライアントやモデルクラスなどのコード生成を行うことができます。

    https://swagger.io/tools/swagger-codegen/

    デフォルト設定で API クライアントを生成する

    最初はデフォルト設定で SwaggerCodegen でコード生成していきましょう。

    まずは Homebrew を使って SwaggerCodegen のインストールを行います。

    1brew install swagger-codegen

    そして、以下のコマンドでコードを生成することができます。

    1swagger-codegen generate \
    2    -i ./sample_swagger.yaml \
    3    -l dart \
    4    -o generated-swagger-files \

    -i で指定している Swagger の API 定義書は、Swagger公式が提供している以下のサンプルを使用しています。

    https://editor.swagger.io/

    上記コマンドによってgenerated-swagger-filesフォルダと、その中に以下のファイルが出力されました。

    出力されたファイル

    ちなみに出力されたファイルがある状態で再度コマンドを実行した場合、上書きはされるんですが不要なファイルが残る可能性があるので、以下のようにフォルダを削除するコマンドを含め sh ファイルにしておくと後々楽になりますね。

    1rm -r generated-swagger-files
    2
    3swagger-codegen generate \
    4    -i ./sample_swagger.yaml \
    5    -l dart \
    6    -o generated-swagger-files \

    実行する時は sh swagger-generate.sh を入力しましょう。

    上記で生成されたファイルの利用方法は README.md に書いてあり、生成されたフォルダで個別にリポジトリを作成し、利用するプロジェクトの pubspec に以下を記入することで利用できます。

    1name: swagger
    2version: 1.0.0
    3description: Swagger API client
    4dependencies:
    5  swagger:
    6    git: https://github.com/GIT_USER_ID/GIT_REPO_ID.git
    7      version: 'any'

    そして以下のように自身のプロジェクト上で API を呼び出すことができるようになりました。

    1import 'package:swagger/api.dart';
    2// TODO Configure OAuth2 access token for authorization: petstore_auth
    3//swagger.api.Configuration.accessToken = 'YOUR_ACCESS_TOKEN';
    4
    5var api_instance = new PetApi();
    6var body = new Pet(); // Pet | Pet object that needs to be added to the store
    7
    8try {
    9    api_instance.addPet(body);
    10} catch (e) {
    11    print("Exception when calling PetApi->addPet: $e\n");
    12}

    今回はこの出力されたファイル構造や、ファイルの中身をいろいろいじっていきましょう!

    ファイルの内容を書き換える

    カスタマイズするためには色々と準備が必要になるので、まずはこちらを対応していきましょう

    ファイルの内容を書き換えるための準備

    SwaggerCodegen で出力されるファイルの中身がどのような記述になっているかは、mustache というファイル形式で定義されています。

    mustache というのは、色々な言語ファイルの出力をテンプレートとして記述できるものです。

    https://mustache.github.io/

    まずはデフォルト設定で指定されている dart の mustache ファイルを見てみましょう。

    https://github.com/swagger-api/swagger-codegen-generators/tree/master/src/main/resources/handlebars/dart

    api_client や model などがあり、どういった種類のファイルのテンプレートなのかがわかりますね。

    この dart フォルダごと、swagger-generate.sh と同じフォルダに移動させ、sh ファイルにテンプレートフォルダの指定を追加しましょう。

    わかりやすくするため、dart フォルダは template という名前に変更してあります。

    1rm -r ../lib/generated-swagger-files
    2
    3swagger-codegen generate \
    4    -i sample_swagger.yaml \
    5    -l dart \
    6    -o generated-swagger-files \
    7    -t template \

    これでデフォルトの生成から、独自のテンプレートを参照しての生成になりました。

    ちなみに SwaggerCodegen の dart のコード生成では null safety 対応がされていないのですが、これを対応するだけであれば、openapi-generatorのdart-dio-next を利用したほうが楽です。

    https://github.com/OpenAPITools/openapi-generator

    ただプラグインを色々使っている関係上カスタマイズしづらいので、カスタマイズ目的なら SwaggerCodegen のほうが楽そうですね。

    これで書き換える準備は完了しましたが、その前に mustache ファイルの中身について理解していきましょう。

    model.mustache ファイルについて

    まずは model クラスの mustache からです。

    コードは以下のようになっています。

    1part of {{pubName}}.api;
    2
    3{{#models}}
    4{{#model}}
    5{{#isEnum}}{{>enum}}{{/isEnum}}{{^isEnum}}{{>class}}{{/isEnum}}
    6{{/model}}
    7{{/models}}

    {{#models}}{{/models}} で、Swagger の yaml ファイルから読み込んだ model データごとに1つの dart ファイルが生成され、{{#model}}{{/model}} の中でそれぞれの model データを取得しています。

    HTML のタグみたいな感じですね。

    そして取り出した model から、{{#isEnum}}{{/isEnum}} で model データが Enum形式だった場合、{{^isEnum}}{{/isEnum}} で Enum 以外だった場合の判定を行なっています。

    {{>enum}}{{>class}} では、それぞれ enum.mustache ファイルと class.mustache ファイルの中身を、そのままこちらにコピーするような形になります。

    なので dart の model.mustache では、単純に Enum かそうでないかを判定して、Swagger の yaml ファイルにある model データごとに dart ファイルを生成しているだけですね。

    class.mustacheファイルについて

    model.mustache から参照しているファイルで、以下のようになっています。

    1class {{classname}} {
    2  {{#vars}}{{#description}}/* {{{description}}} */{{/description}}
    3  {{^isEnum}}
    4  {{{datatype}}} {{name}} = {{{defaultValue}}};
    5  {{/isEnum}}
    6  {{#isEnum}}
    7  {{{datatype}}} {{name}} = null;
    8  {{/isEnum}}
    9  {{#allowableValues}}
    10  {{#min}} // range from {{min}} to {{max}}{{/min}}//{{^min}}enum {{name}}Enum { {{#values}} {{.}}, {{/values}} };{{/min}}
    11  {{/allowableValues}}
    12  {{/vars}}
    13
    14  {{classname}}();
    15
    16  @override
    17  String toString() {
    18    return '{{classname}}[{{#vars}}{{name}}=${{name}}, {{/vars}}]';
    19  }
    20
    21  {{classname}}.fromJson(Map<String, dynamic> json) {
    22    if (json == null) return;
    23  {{#vars}}
    24  {{#isDateTime}}
    25    {{name}} = json['{{baseName}}'] == null ? null : DateTime.parse(json['{{baseName}}']);
    26  {{/isDateTime}}
    27  {{^isDateTime}}
    28    {{name}} = {{#complexType}}{{#isListContainer}}{{complexType}}.listFromJson(json['{{baseName}}']){{/isListContainer}}{{^isListContainer}}{{#isMapContainer}}{{complexType}}.mapFromJson(json['{{baseName}}']){{/isMapContainer}}{{^isMapContainer}}new {{complexType}}.fromJson(json['{{baseName}}']){{/isMapContainer}}{{/isListContainer}}{{/complexType}}{{^complexType}}{{#isListContainer}}(json['{{baseName}}'] as List).map((item) => item as {{items.datatype}}).toList(){{/isListContainer}}{{^isListContainer}}json['{{baseName}}']{{/isListContainer}}{{/complexType}};
    29  {{/isDateTime}}
    30  {{/vars}}
    31  }
    32
    33  Map<String, dynamic> toJson() {
    34    return {
    35    {{#vars}}
    36      {{#isDateTime}}'{{baseName}}': {{name}} == null ? '' : {{name}}.toUtc().toIso8601String(){{^@last}},{{/@last}}{{/isDateTime}}{{^isDateTime}}'{{baseName}}': {{name}}{{^@last}},{{/@last}}{{/isDateTime}}
    37    {{/vars}}
    38     };
    39  }
    40
    41  static List<{{classname}}> listFromJson(List<dynamic> json) {
    42    return json == null ? new List<{{classname}}>() : json.map((value) => new {{classname}}.fromJson(value)).toList();
    43  }
    44
    45  static Map<String, {{classname}}> mapFromJson(Map<String, Map<String, dynamic>> json) {
    46    var map = new Map<String, {{classname}}>();
    47    if (json != null && json.length > 0) {
    48      json.forEach((String key, Map<String, dynamic> value) => map[key] = new {{classname}}.fromJson(value));
    49    }
    50    return map;
    51  }
    52}

    これを使った出力前と出力後を見てみましょう。

    以下が Swagger の yaml にある User のレスポンス仕様です。

    Swagger の yaml にある User のレスポンス仕様

    以下が出力されたファイルになります。

    いくつか被ってる部分があるので、fromJson 以降は省略しています。

    1part of swagger.api;
    2
    3class User {
    4  
    5  int id = null;
    6
    7  String username = null;
    8
    9  String firstName = null;
    10
    11  String lastName = null;
    12
    13  String email = null;
    14
    15  String password = null;
    16
    17  String phone = null;
    18/* User Status */
    19  int userStatus = null;
    20
    21  User();
    22
    23  @override
    24  String toString() {
    25    return 'User[id=$id, username=$username, firstName=$firstName, lastName=$lastName, email=$email, password=$password, phone=$phone, userStatus=$userStatus, ]';
    26  }
    27
    28  User.fromJson(Map<String, dynamic> json) {
    29    if (json == null) return;
    30    id = json['id'];
    31    username = json['username'];
    32    firstName = json['firstName'];
    33    lastName = json['lastName'];
    34    email = json['email'];
    35    password = json['password'];
    36    phone = json['phone'];
    37    userStatus = json['userStatus'];
    38  }

    上から順番に見ていきましょう。

    {{classname}} ではその名の通りクラス名が出力されていますね。

    変数宣言ではコメント出力部分がいくつかありますが、肝心なところは{{#vars}} {{{datatype}}} {{name}} = {{{defaultValue}}}; {{/vars}} で出力されています。

    また json からモデルデータに変換するということで、json から取得する各変数のキーについても、{{#vars}}{{baseName}}{{/vars}} で出力しています。

    このような感じで、{{}} があって少しとっつき難いところはありますが、出力前と出力後と mustache ファイルを見ればなんとなくわかるようになってます。

    ファイルの内容を書き換える

    基本的には mustache にあるコードを変更するだけで、好きなように書き換えることが可能です。

    例えば hoge() という関数をそれぞれの class で持ちたい場合、以下のようにするだけで生成される class ファイル全てに hoge() が実装されます。

    1class {{classname}} {
    2  ...
    3
    4  void hoge() { 
    5    print("hoge");
    6  }
    7}

    ファイルごとに何か変更するだけであれば、これだけで解決するのですが、例えば Utility 的なファイルが一つ欲しいとか、ファイル構成自体を変えたい場合などはもう少し踏み込んだ作業が必要になります。

    次はそちらを試していきましょう。

    ファイル構成を変更する

    ファイル構成を変更する場合は、SwaggerCodegen のジェネレータを変更する必要があります。

    手順に関してはこちらに記載されていますので、まずはジェネレータを変更するための環境構築をしていきましょう。

    ファイル構成を変更するための環境構築

    今回は Docker を使って開発していきます。

    記載されているコマンドを打つだけでできるのですが、その前に色々必要になるものをインストールしておきましょう。

    まずは Homebrew を使って Maven と wget をインストールしておきます。

    1brew install maven
    2brew install wget

    あとは Docker をインストールしておきましょう。

    公式サイトでインストールした後、起動して Docker コマンドが打てる状態にまでしておきます。

    これでコマンドを打ってもエラーにならなくなったはずなので、SwaggerCodegen にある手順を進めていきましょう!

    まずは SwaggerCodegen を git から落とします。

    この記事を書いた時点では、v2が master になっているので、Tag などからv3に変更しておいてください。

    その後に落としてきた SwaggerCodegen へ cd コマンドで移動して、以下のコマンドを実行しましょう。

    1sh run-in-docker.sh mvn package

    あとはこちらにあるコマンドをコピペ(#コメントは除く)して入れていきます。

    以下のコマンドが最初にありますが、generator-stub-docker.sh の実行で、generate コマンドの-l で指定するものが決まったりするので、それだけ除いてコピペするのをおすすめします。

    1# project dir
    2TARGET_DIR=/tmp/codegen/mygenerator
    3mkdir -p $TARGET_DIR
    4cd $TARGET_DIR
    5# generated code location
    6GENERATED_CODE_DIR=generated
    7mkdir -p $GENERATED_CODE_DIR
    8# download desired version
    9wget https://repo1.maven.org/maven2/io/swagger/codegen/v3/swagger-codegen-cli/3.0.21/swagger-codegen-cli-3.0.21.jar -O swagger-codegen-cli.jar
    10wget https://raw.githubusercontent.com/swagger-api/swagger-codegen/3.0.0/standalone-gen-dev/docker-stub.sh -O docker-stub.sh
    11wget https://raw.githubusercontent.com/swagger-api/swagger-codegen/3.0.0/standalone-gen-dev/generator-stub-docker.sh -O generator-stub-docker.sh
    12chmod +x *.sh

    続けて generator-stub-docker.sh を実行しましょう。

    2箇所ある custom の部分は好きなように変更してください。

    1# generated initial stub: -p <root package> -n <generator name>
    2sh generator-stub-docker.sh -p io.swagger.codegen.custom -n custom

    これで tmp/codegen/mygenerator 下に色々とファイルが作成されました。

    mygenerator

    tmp フォルダは隠しフォルダになってるので、Finder で見たい場合は command + shift + .で隠しフォルダを表示するようにしておきましょう。

    src フォルダの中にはジェネレータである CustomGenerator.java と、mustache で作られたテンプレートファイルが作成されています。

    mygenerator srcフォルダ内のデータ

    META-INF フォルダの中にある CodegenConfig ファイルにはジェネレータ名が記載されています。

    ジェネレータのクラス名・ファイル名と連動しているので、そちらを変更した場合はこちらも変更するようにしておきましょう。

    1io.swagger.codegen.custom.CustomGenerator

    src の中身が作成されたことで、続けて以下のコマンドを入力して petstore-simple.yaml からコードを生成することができます。

    1wget https://raw.githubusercontent.com/swagger-api/swagger-codegen/3.0.0/modules/swagger-codegen/src/test/resources/3_0_0/petstore-simple.yaml -O petstore-simple.yaml
    2wget https://raw.githubusercontent.com/swagger-api/swagger-codegen/3.0.0/standalone-gen-dev/run-in-docker.sh -O run-in-docker.sh
    3wget https://raw.githubusercontent.com/swagger-api/swagger-codegen/3.0.0/standalone-gen-dev/docker-entrypoint.sh -O docker-entrypoint.sh
    4chmod +x *.sh
    5sh run-in-docker.sh generate -i petstore-simple.yaml -l custom -o /gen/$GENERATED_CODE_DIR

    2回目以降は最後の行にあるコマンドだけで大丈夫です。

    このコマンドを実行することで、mygenerator 下にある generated フォルダに色々と生成されます。

    mygenerator 下にある generated フォルダに色々と生成される

    ここまでの手順は、swagger-codegen generate の時と若干異なっていますが、最終的なコマンドはだいたい同じですね。

    ちなみに generate した辺りから、mygenerator フォルダの直下にtagertフォルダが生成されています。

    この中身には mustache ファイルなどが入っており、2回目以降はおそらくここを参考に generate しているっぽいので、変更したのに反映されていない場合は target フォルダごと削除することで解決するかと思います。

    これで環境構築はできたので、ジェネレータのコードを見つつ編集していきましょう!

    CustomGenerator.java について

    そこそこ長いコードになっているので、重要なコード以外は省いてます。

    1package io.swagger.codegen.custom;
    2
    3import io.swagger.codegen.v3.*;
    4import io.swagger.codegen.v3.generators.DefaultCodegenConfig;
    5
    6import java.util.*;
    7import java.io.File;
    8
    9public class CustomGenerator extends DefaultCodegenConfig {
    10
    11  // source folder where to write the files
    12  protected String sourceFolder = "src";
    13  protected String apiVersion = "1.0.0";
    14
    15  /*
    16   * -lタグで指定する名前
    17   * 
    18   * @return the friendly name for the generator
    19   */
    20  public String getName() {
    21    return "custom";
    22  }
    23
    24  public CustomGenerator() {
    25    super();
    26
    27    // set the output folder here
    28    outputFolder = "generated-code/custom";
    29
    30    /**
    31     * swaggerの定義ファイルから、modelごとに生成するテンプレートを指定
    32     */
    33    modelTemplateFiles.put(
    34      "model.mustache", // テンプレートファイル名
    35      ".sample");       // 生成するファイルの種類(.dartとか自由に指定できる)
    36
    37    /**
    38     * swaggerの定義ファイルから、apiごとに生成するテンプレートを指定
    39     */
    40    apiTemplateFiles.put(
    41      "api.mustache",   // テンプレートファイル名
    42      ".sample");       // 生成するファイルの種類
    43
    44    /**
    45     * テンプレートを読み取る場所
    46     */
    47    templateDir = "custom";
    48
    49    /**
    50     * apiTemplateFilesで指定したファイルが生成される場所
    51     */
    52    apiPackage = "io.swagger.client.api";
    53
    54    /**
    55     * modelTemplateFilesで指定したファイルが生成される場所
    56     */
    57    modelPackage = "io.swagger.client.model";
    58
    59
    60    /**
    61     * テンプレートファイルに渡せる値
    62     * {{apiVersion}}で出力できるようになる
    63     */
    64    additionalProperties.put("apiVersion", apiVersion);
    65
    66    /**
    67     * modelでもapiでもないファイルをこちらで指定する
    68     * READMEとか共通したいクラスとか
    69     */
    70    supportingFiles.add(new SupportingFile("myFile.mustache",   // テンプレート名
    71      "",                                                       // 出力先
    72      "myFile.sample")                                          // 出力するファイル名
    73    );
    74  }
    75}

    上から順に見ていきましょう。

    getName メソッドで-l dart のような指定する名前を決めることができます。

    CustomGenerator() コンストラクタでは、主にどの mustache ファイルを読み込むかなどを定義していますね。

    modelTemplateFilesapiTemplateFiles では複数ある Model や API ごとにファイルを生成したい場合に利用します。

    これは複数 put することで複数種類のファイルを Model と API ごとに生成することが可能です。

    modelPackageapiPackage では生成先のフォルダを指定することができます。

    現状の生成先にフォルダがない場合は勝手にフォルダが作られるので、ファイル構成を変更したい場合はこちらを変更してあげましょう。

    templateDir は mustacheファイルを読み込む場所を決めています。

    上記コードからは省いていますが、templateDir は CustomGeneratorのgetTemplateDir メソッドに利用されている変数です。

    Docker で生成する場合、templateDir は必須の項目になるのでコピペ時に消えてしまわないように注意しておきましょう!

    supportingFiles でも出力先の引数があるので、こちらで自由に変更することができます。

    ここで省略した部分だったり、自分が生成したい言語の場合はどうしたらいいの?という疑問があると思いますが、

    そういった場合は以下の URL から確認してみてください。

    既に定義されている言語がある場合は、それをコピペしてから編集していくのが一番楽ですね。

    https://github.com/swagger-api/swagger-codegen-generators/tree/master/src/main/java/io/swagger/codegen/v3/generators

    親クラスである DefaultCodegenConfig も同じ階層にあるので、そちらも確認してみてください。

    これである程度はファイル構成を変更できるようになったと思います。

    TIPS

    ここからは、私自身がジェネレータを編集していく中で、詰まった部分と解決方法をいくつか紹介します。

    mustache に出力する文字列をキャメルケースにしたい

    additionalProperties.put でキャメルケースにした文字列を mustache に送ること自体は簡単なのですが、既にある{{}}タグの出力をキャメルケースにする方法です。

    まずはジェネレータで以下のようにコードを追加しましょう。

    1import io.swagger.codegen.v3.generators.handlebars.lambda.CamelCaseLambda;
    2...
    3additionalProperties.put("camelcase", new CamelCaseLambda().generator(this));

    import では以下の場所から CamelCaseLambda を取得しています。

    https://github.com/swagger-api/swagger-codegen-generators/tree/master/src/main/java/io/swagger/codegen/v3/generators/handlebars/lambda

    他のケースもあるのでそれらも同様に取得して使用することができると思います。

    上記のコードが入ったジェネレータを実装後、mustacheファイルで以下のようにすることで、部分的にキャメルケースを適用できます。

    1// 「hogeFoo」になる
    2{{#camelcase}}hoge_foo{{/camelcase}}

    複数種類のAPI/Modelファイルを生成したい

    APIの場合、apiTemplateFilesに複数putすることで複数種類のAPIファイルが生成されますが、デフォルト設定だとAPI名がそのままファイル名になるため、どちらかが上書きされてしまいます。

    それを回避するためにはジェネレータにて以下のように、mustacheファイルごとにファイル名を書き換えましょう。

    1apiTemplateFiles.put("api.mustache", ".dart");
    2apiTemplateFiles.put("api_repository.mustache", ".dart");
    3...
    4@Override
    5public String apiFilename(String templateName, String tag) {
    6    String result = super.apiFilename(templateName, tag);
    7    String repositoryFolder = outputFolder + File.separator + "repository";
    8    String fileType = ".dart";
    9    if ( templateName.endsWith("repository.mustache") ) {
    10        int ix = result.lastIndexOf('/');
    11        result = result.substring(0, ix) + result.substring(ix, result.length() - fileType.length()) + "_repository.dart";
    12        result = result.replace(apiFileFolder(), repositoryFolder);
    13    }
    14    return result;
    15}

    apiFilenameメソッドでは、その名の通りapiTemplateFilesより生成されるAPIファイルの名前を決めるものです。

    templateNameから、どのmustacheをテンプレートとしてファイルを生成するかがわかるので、それによって返却値を変えているだけです。

    また返却値にはフォルダの情報も含まれているので、種類ごとに別フォルダに出力したい場合にもこちらで対応できますね。

    まとめ

    今回はSwaggerCodegenによるコード生成をカスタマイズする方法を紹介しました。

    この方法ではSwaggerCodegenで定義されている言語以外のものでも生成できるので、興味があれば是非活用してみてください!

    りゅうちゃん(エンジニア)

    りゅうちゃん(エンジニア)

    おすすめ記事

    GitHubActionsのランナーに触れてみた

    こやまん(エンジニア)

    こやまん(エンジニア)

    2024.03.28

    IT技術

    Azure Data FactoryでSlackへ通知をしてみる

    たかやん(エンジニア)

    たかやん(エンジニア)

    2024.03.28

    IT技術

    GCP Secret Managerを使ってみた

    たなゆー(エンジニア)

    たなゆー(エンジニア)

    2024.03.21

    IT技術

    Bitriseのパイプラインと環境変数

    加納(エンジニア)

    加納(エンジニア)

    2024.03.11

    IT技術