DEV Community

Cover image for OpenApiWeaver: Generate Type-Safe C# Clients from OpenAPI at Compile Time with Source Generators
Tatsuro Shibamura for Polymind

Posted on • Originally published at blog.shibayan.jp

OpenApiWeaver: Generate Type-Safe C# Clients from OpenAPI at Compile Time with Source Generators

TL;DR

I built OpenApiWeaver, a C# library that generates HTTP clients from OpenAPI definitions at compile time using Source Generators. Just drop your OpenAPI file into your project, add one ItemGroup entry, and build — no codegen CLI, no reflection at runtime, full NRT and required support, and clean handling of string-based enums via readonly record struct.

GitHub logo shibayan / openapi-weaver

OpenAPI documents into strongly typed C# HTTP clients at build time with an incremental Roslyn source generator.

OpenApiWeaver

CI Downloads NuGet License

OpenApiWeaver is an incremental Roslyn source generator that turns OpenAPI 3.x documents, including OpenAPI 3.2, into strongly typed C# HTTP clients at build time. No runtime code generation, no reflection - just plain C# emitted during compilation.

Quick Start

1. Install the package

<ItemGroup>
  <PackageReference Include="OpenApiWeaver" Version="x.y.z" PrivateAssets="all" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

2. Add your OpenAPI document

<ItemGroup>
  <OpenApiWeaverDocument Include="openapi\petstore.yaml"
                         ClientName="PetstoreClient"
                         Namespace="Contoso.Generated" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

Use OpenApiWeaverDocument rather than AdditionalFiles; the package's MSBuild targets project these items into compiler inputs automatically.

3. Use the generated client

var client = new PetstoreClient(accessToken: "your-token");
// Operations are grouped by OpenAPI tag
var pet = await client.Pets.GetAsync(petId: 1
Enter fullscreen mode Exit fullscreen mode

Why another OpenAPI client generator?

If you've needed a C# client from an OpenAPI definition, you've probably reached for OpenAPI Generator, AutoRest, or NSwag. They all work, but each has its quirks, and I've often found myself compromising on one thing or another.

What I really wanted was dead simple: drop an OpenAPI file in my project, and have the client appear seamlessly at build time — no external tools, no checked-in generated files, no separate codegen step in CI. That's exactly what Source Generators are for, so I built OpenApiWeaver.

Design goals

The core idea is to push as much work as possible into compile time via Source Generators. This gives us two nice properties:

  • No runtime reflection in the generated code itself
  • Type-safe output that takes full advantage of modern C# features

Because the generated code leans on recent language features, consumers need .NET 8 or later. In practice, this shouldn't be a real constraint for anyone today.

There are plenty of similar libraries out there, so I put a lot of effort into the quality of the generated code. Specifically:

  • Nullable reference types and required modifiers are applied faithfully based on the OpenAPI schema
  • Enums — particularly string-based ones — are generated in a form that's actually pleasant to use from C#
  • XML doc comments are emitted from OpenAPI description and summary fields, so IntelliSense just works

The generated client code itself contains no reflection. However, because it relies on System.Text.Json, reflection is still used internally there. That means NativeAOT is not supported yet — but once the System.Text.Json source generator can be composed cleanly with this one, it should be achievable.

Usage

Usage is about as simple as I could make it: install the OpenApiWeaver package from NuGet, and you're done. Once installed, you get a new MSBuild item type called OpenApiWeaverDocument. Point its Include attribute at your OpenAPI definition file:

<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>
Enter fullscreen mode Exit fullscreen mode

OpenApiWeaverDocument also accepts ClientName and Namespace attributes, which let you override the generated client's class name and namespace.

What you get after a build

That's the whole setup. Build the project, and the client is generated from the OpenAPI definition. For larger specs, the generator splits clients per OpenAPI Tag, so you end up with one client class per logical grouping in your API — which keeps things manageable.

Generated clients split per OpenAPI tag

The sample petstore.json has Pet, Store, and User tags, and you can see a client was generated for each.

Each client exposes its operations as methods, so for the most part you can drive development purely through IntelliSense. (The naming of auto-generated methods still has room for improvement, I'll admit.)

IntelliSense showing generated client methods

When the OpenAPI definition includes description or summary fields, those flow through to XML doc comments on the generated classes and methods, so you get nice tooltips in the editor:

Tooltip showing XML doc comments from OpenAPI descriptions

The generated type definitions

Operations are relatively straightforward, but the type definitions generated from Schemas are where I spent most of my effort. Take the Pet class from the sample — here's what actually gets generated:

/// <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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

A few things worth pointing out:

  • NRT and required are applied correctly, so mistakes get caught at compile time
  • String-based enums are not mapped to C# enum. Instead they become readonly record struct, which lets you treat them as strings while still keeping named well-known values like Available, Pending, Sold

This string-enum pattern has become pretty common in modern .NET libraries, and I think it strikes a good balance between type safety and the reality that string enums in OpenAPI often need to tolerate unknown values.

There are a few other niceties I won't cover here — the full documentation goes into more detail:

🔗 OpenApiWeaver documentation

A note on the OpenAPI parser

OpenApiWeaver only owns the "generate a client from an already-parsed OpenAPI document" half of the problem. Parsing itself is delegated to Microsoft.OpenApi, which keeps the surface area small. The docs say OpenAPI 3.x is supported, but really it's whatever that library supports:

🔗 OpenAPI.NET release announcements (Microsoft DevBlogs)

Closing note: this was built with AI coding tools

One last thing worth sharing. This entire library — Source Generator, runtime pieces, and documentation — was built heavily with AI coding assistance. I did the PoC in Codex with GPT-5.4, then moved to GitHub Copilot Chat using GPT-5.4 and Opus 4.6 High for the actual implementation.

Honestly, I've always considered hand-writing Source Generator code to be a painful endeavor, and I'd been putting off projects like this for exactly that reason. Leaning on AI coding tools let me focus on design decisions — API shape, code quality goals, naming — while the assistant handled most of the mechanical work of emitting C# source.

From PoC to release, including docs, it took about three days of real work. That still surprises me.

If you try OpenApiWeaver out, I'd love to hear what you think — issues and PRs welcome on the repo.

Top comments (0)