openapi-generator で ASP.NET Core のコードを生成する(のがいいのかわからない)

OpenAPI の定義があることのメリットの一つは自動生成だと自分では勝手に思っているんだけど、ちょっとその感覚がいまいちなのかと揺らいできた。

Web APIASP.NET Core + Client 側 TypeScript でやっている場合

例えば

class Member 
{
    public int Id { get; }
    puiblic int Name { get; }
}

みたいな定義があり、これを Controller 側では

[ProducesResponseType(typeof(List<Member>), 200)]
public IActionResult GetMembers()

みたいに書くとする。(手で書いてるので間違いはあるかもしれないが、雰囲気で)

これの swagger 定義はだいたい以下のようになる。(ASP.NET Core で出力する場合はもうちょいいろいろ出るかもしれないが、大事な部分だけということで)

{
    "openapi": "3.0.1",
    "paths": {
        "/members": {
            "get": {
                "operationId": "GetMembers",
                "responses": {
                    "200": {
                        "description": "Success",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/Member"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "Member": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    }
                }
            }
        }
    }
}

ここでポイントは Memberrequired 属性が無いことである。

この辺はどの JSON Serializer を使うかにもよると思うが、概ね C# の class 側に [Required] の attribute を付けることになる。

で、これを openapi-generator にくわせて typescript-fetch で出力すると、出てくる TS 側の Member は以下になる。

export interface Member {
    readonly id?: number;
    readonly name?: string | null;

当たり前なのだが、すべて optional property になってしまう。Client 側からは 「Id さえもらえないことあるの?」的なことになる。

何が言いたいのか

OpenAPI の定義があることで、Web API / Client でミスマッチなくデータのやり取りができるようになったかと思いきや、ミスマッチを起こさないためには、双方がある程度気を使って開発をしなければいけないということ。

まあ、当たり前っちゃ当たり前ですし、JSON Schema 手書きとかしてた時代を考えると Web Framework で勝手に作ってくれるだけでもすごい楽は楽なんですが。

じゃあ、Web API 側も自動生成しちまえばいいじゃないか

OpenAPI を使ってみた系の記事を見ていると、中には定義を中心にして Web API / Client の両方を生成している例もちらほらあった。

であれば、と思い自分でもやってみた。

生成

とりあえず動かしたかったので、library としてではなく、ASP.NET Core app として生成した。ちなみに library で生成しても Controller も abstract で生成してくれる。

npx openapi-generator generate -g aspnetcore -i openapi.json -p aspnetCoreVersion=3.1,generateBody=true,isLibrary=false

model 側

    [DataContract]
    public partial class Member : IEquatable<Member>
    {
        /// <summary>
        /// Gets or Sets Id
        /// </summary>
        [Required]
        [DataMember(Name="id", EmitDefaultValue=false)]
        public int Id { get; set; }

        /// <summary>
        /// Gets or Sets Name
        /// </summary>
        [Required]
        [DataMember(Name="name", EmitDefaultValue=false)]
        public string Name { get; set; }

Controller側

        [HttpGet]
        [Route("/members")]
        [ValidateModelState]
        [SwaggerOperation("GetMembers")]
        [SwaggerResponse(statusCode: 200, type: typeof(List<Member>), description: "Success")]
        public virtual IActionResult GetMembers();

一応だが、async にするとかは、オプションで切り替えることはできる。

生成してみて

  • System.Text.Json ではなく Newtonsoft.Json を使っている
    • Enum も Members に加えてみたが、converter も生成された
  • Controller も ASP.NET Core が用意している [ProducesResponseType] 等を利用せず Swashbuckle の機能をそのまま利用している。
  • 検索等でありがちな、optional な query parameter に関しても、C# 8 を想定すれば Nullable でカバーできそうだが、そもそも controller の引数に現れない

と、軽く列挙してみたが、言語や Web Framework の進化への追随という意味では ASP.NET Core に関していえば、openapi-generator の生成結果は、あまり満足いくものではないのかなと思う。

この辺りは、強調したとおりで、他の言語やフレームワークに関しては十分追随できているのかもしれない。(利用者や貢献者のボリュームとかもあるだろうし)

つかうべきか?

Pull Requests · OpenAPITools/openapi-generator · GitHub

上記は Server C-Sharp label のついた openapi-generator の PR だが、まああまり無い。

以下に openapi-generator の aspnetcore 生成で使えるオプションがある。

openapi-generator/AspNetCoreServerCodegen.java at master · OpenAPITools/openapi-generator · GitHub

modelClassModifiler があるので、以下のテンプレートをいじれば model に関してはうまくやれる可能性はある。

openapi-generator/model.mustache at master · OpenAPITools/openapi-generator · GitHub

そうすれば、少なくとも Web API の層で使うモデル (DTO とでもいうのか?)は自動生成されたものを使えるので、Client 側とのミスマッチは防げるかもしれない。

公開した後に思ったが、モデルだけ生成でいいんだったら、openapi-generator じゃなくて、他に C# で書かれた Client 生成用のツールとかでいいんじゃないかという気がする。

大きなお悩みポイント

開発リソースの少ない現場で、できるだけ成果を最大化しようと考えると、極力定型的な作業は減らして(定型的なコードもコードレビューしてるとそれだけで時間を食う)自動生成に頼れる部分は頼りたいという気持ちがあって、openapi-generator に食いついてみた。

ただ、自動生成は自動生成で、生成されたものがそのまま使えないと、今度はそれにパッチを当てたり workaround を考えたりと、逆に分かりにくくなってしまって辛くなる。

割と人の出入りも多い現場なので(退職どうこうというより、業務委託率が高い)、とにかく覚えてもらうことを少なくしつつ、早く(かつ、品質も保ちつつ)成果を出せる仕組みを考えたいなと思うが、そういうのってホント難しいんだなと実感している。