しばやん雑記

Azure とメイドさんが大好きなプログラマーのブログ

C# の Source Generator を使って OpenAPI 定義からクライアントを生成するライブラリを公開しました

これまで OpenAPI 定義から C# 向けのクライアントが必要な場合は OpenAPI Generator や AutoGen、あるいは NSwag といったツールを使ってきたのですが、それぞれのツールに癖があり割と妥協して使うことが多かったです。個人的には OpenAPI 定義だけ置いておけばビルド時にクライアントが生成されて、シームレスに使えることを重視したかったので Source Generator を使って OpenApiWeaver というライブラリを実装しました。

特徴としては可能な限り Source Generator を使ってコンパイル時に生成することで、リフレクションを避けつつ型安全なコードを生成するようにしています。最新の C# 機能を使ったコードを生成するので、利用側は .NET 8 以降が必要ですが現実的には問題にならないはずです。

同様のライブラリは存在すると思いますが、生成されるコードの質について力を入れています。具体的には NRT や required といったアノテーションを OpenAPI 定義に従って付与されるようになっていますし、enum の扱いについても数値と文字列で扱いやすいコードを生成するようにしています。

生成されたクライアントにはリフレクションに依存するコードが存在しないですが、System.Text.Json に依存する部分で内部的にリフレクションが使われているため、現状は NativeAOT には対応できていませんが、Source Generator の組み合わせが可能になれば対応できるはずです。

使い方は非常にシンプルで NuGet から OpenApiWeaver パッケージをインストールするだけで完結します。パッケージをインストールすると OpenApiWeaverDocument という要素が使えるようになるので、Include 属性で OpenAPI 定義のファイル名を指定します。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="OpenApiWeaver" Version="1.0.0" />
  </ItemGroup>

  <ItemGroup>
    <OpenApiWeaverDocument Include="petstore.json" ClientName="PetStoreClient" />
  </ItemGroup>

</Project>

OpenApiWeaverDocument には追加の属性として ClientNameNamespace を用意しているので、指定することで生成されるクライアントのクラス名と名前空間をオーバーライド可能です。

設定が終わればビルドするだけで、以下のように OpenAPI 定義に従ってクライアントが生成されます。大規模な OpenAPI 定義であっても適切に Tags が設定されていれば、それぞれの Tag 単位でクライアントを生成する仕組みになっています。

今回のサンプルで使った petstore.json には Pet / Store / User のタグが存在するので、それぞれに対してクライアントが生成されていることが分かります。

それぞれのクライアントには以下のようにメソッドが定義されているので、基本的には IntelliSense だけで開発できるような状態になっています。自動生成されるメソッドの命名については改善の余地があるとは思っています。

自動生成されるクラスやメソッドに対しては OpenAPI 定義で Description や Summary が定義されていれば、それを XML Document Comment として出力するようにしているので、定義されていれば以下のようにツールチップで確認可能です。

OpenAPI 定義でいうところの Operations については割とシンプルではあるのですが、Schemas から生成される型定義については前述したように力を入れています。今回の例でいうと Pet クラスがあるのですが、実際に生成されたコードは以下のようになっています。

NRT や required を適切に設定したクラス定義が生成され、コンパイル時にエラーを発見できるようにしています。特徴的なのは文字列ベースの enum の扱いで、あえて C# の enum 型にマッピングすることなく readonly record struct 型を使うことで文字列のまま扱えるようにしています。

/// <summary>
/// Pet
/// </summary>
public sealed class Pet
{
    /// <summary>
    /// Id
    /// </summary>
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    [JsonPropertyName("id")]
    public long? Id { get; init; }
    /// <summary>
    /// Name
    /// </summary>
    [JsonPropertyName("name")]
    public required string Name { get; init; }
    /// <summary>
    /// Category
    /// </summary>
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    [JsonPropertyName("category")]
    public Category? Category { get; init; }
    /// <summary>
    /// PhotoUrls
    /// </summary>
    [JsonPropertyName("photoUrls")]
    public required IReadOnlyList<string> PhotoUrls { get; init; }
    /// <summary>
    /// Tags
    /// </summary>
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    [JsonPropertyName("tags")]
    public IReadOnlyList<Tag>? Tags { get; init; }
    /// <summary>
    /// Status
    /// </summary>
    /// <remarks>
    /// pet status in the store
    /// </remarks>
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    [JsonPropertyName("status")]
    public Pet.StatusEnum? Status { get; init; }

    /// <summary>
    /// Pet.StatusEnum
    /// </summary>
    /// <remarks>
    /// pet status in the store
    /// </remarks>
    [JsonConverter(typeof(StatusEnumJsonConverter))]
    public readonly record struct StatusEnum(string Value)
    {
        public static readonly StatusEnum Available = new("available");
        public static readonly StatusEnum Pending = new("pending");
        public static readonly StatusEnum Sold = new("sold");

        public override string ToString() => Value;
    }

    public sealed class StatusEnumJsonConverter : JsonConverter<StatusEnum>
    {
        public override StatusEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return new StatusEnum(reader.GetString()!);
        }

        public override void Write(Utf8JsonWriter writer, StatusEnum value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value.Value);
        }
    }
}

この方法は最近のライブラリではよく使われているので、個人的にも扱いやすいものになっていると考えています。それ以外にも工夫している点はあるのですが、詳細はドキュメントに記載しているので確認してもらえればと思います。

OpenApiWeaver は読み込み済みの OpenAPI 定義からクライアントを生成する Source Generator の提供だけを行い、定義の読み込み自体は Microsoft.OpenApi を使うことで簡略化しています。ドキュメントには OpenAPI 3.x のサポートと書いていますが、このライブラリが対応しているバージョンであれば利用可能です。

最後になりますが、このライブラリは Codex App の GPT-5.4 を使って PoC を行った後に GitHub Copilot Chat の GPT-5.4 と Opus 4.6 High を使って実装しました。これまで Source Generator のコードを手書きするのはハードすぎると考えていたのですが、AI コーディングをフル活用することで自分は設計に特化することが出来ました。

ドキュメントも含めて全て AI コーディングで実装されているので、PoC からリリースまで実質 3 日程で完了しました。