OpenAPI Client Generation

Generate type-safe C# clients from OpenAPI 3.x specifications with automatic Result types and exhaustiveness checking.

Installation

Install the generator CLI tool:

dotnet tool install -g RestClient.Net.OpenApiGenerator.Cli

Or add to your project:

dotnet add package RestClient.Net.OpenApiGenerator

Quick Start

Generate a client from an OpenAPI spec:

restclient-gen \
  --input https://petstore3.swagger.io/api/v3/openapi.json \
  --output Generated \
  --namespace PetStore.Client

Or from a local file:

restclient-gen \
  --input api.yaml \
  --output Generated \
  --namespace MyApi.Client

CLI Options

Option Short Description
--input -i OpenAPI spec URL or file path (required)
--output -o Output directory (default: Generated)
--namespace -n C# namespace for generated code (required)
--tags -t Filter operations by tags (comma-separated)
--skip-validation Skip OpenAPI spec validation

Examples

Generate only specific tags:

restclient-gen \
  -i api.yaml \
  -o Generated \
  -n MyApi.Client \
  --tags "Users,Orders"

Generated Files

The generator creates:

Generated/
├── Models/
│   ├── User.cs
│   ├── Order.cs
│   └── ...
├── HttpClientExtensions.g.cs
├── Deserializers.g.cs
└── GlobalUsings.g.cs

Models

Data transfer objects from OpenAPI schemas:

// Generated/Models/User.cs
namespace MyApi.Client.Models;

public sealed record User(
    string Id,
    string Name,
    string Email,
    DateTime CreatedAt
);

HttpClient Extensions

Extension methods for each operation:

// Generated/HttpClientExtensions.g.cs
namespace MyApi.Client;

public static class HttpClientExtensions
{
    public static Task<Result<User, HttpError<ApiError>>> GetUserByIdAsync(
        this HttpClient httpClient,
        string userId,
        CancellationToken ct = default) =>
        httpClient.GetAsync(
            url: $"/users/{userId}".ToAbsoluteUrl(),
            deserializeSuccess: Deserializers.User,
            deserializeError: Deserializers.ApiError,
            cancellationToken: ct
        );

    public static Task<Result<User, HttpError<ApiError>>> CreateUserAsync(
        this HttpClient httpClient,
        CreateUserRequest request,
        CancellationToken ct = default) =>
        httpClient.PostAsync(
            url: "/users".ToAbsoluteUrl(),
            body: request,
            serializeRequest: Serializers.Json,
            deserializeSuccess: Deserializers.User,
            deserializeError: Deserializers.ApiError,
            cancellationToken: ct
        );
}

Type Aliases

Automatically generated for clean pattern matching:

// Generated/GlobalUsings.g.cs
global using OkUser = Outcome.Result<User, Outcome.HttpError<ApiError>>
    .Ok<User, Outcome.HttpError<ApiError>>;

global using ErrorUser = Outcome.Result<User, Outcome.HttpError<ApiError>>
    .Error<User, Outcome.HttpError<ApiError>>;

global using ResponseErrorUser = Outcome.HttpError<ApiError>.ErrorResponseError;
global using ExceptionErrorUser = Outcome.HttpError<ApiError>.ExceptionError;

Using Generated Code

using MyApi.Client;
using MyApi.Client.Models;

// Create HttpClient (use IHttpClientFactory in production)
using var httpClient = new HttpClient
{
    BaseAddress = new Uri("https://api.example.com")
};

// Use generated extension methods
var result = await httpClient.GetUserByIdAsync("123");

// Pattern match with generated type aliases
var message = result switch
{
    OkUser(var user) => $"Found: {user.Name} ({user.Email})",
    ErrorUser(ResponseErrorUser(var err, var status, _)) => $"API Error {status}: {err.Message}",
    ErrorUser(ExceptionErrorUser(var ex)) => $"Exception: {ex.Message}",
};

Console.WriteLine(message);

OpenAPI Spec Requirements

Supported Features

  • Path parameters
  • Query parameters
  • Request bodies (JSON)
  • Response bodies (JSON)
  • Schema references ($ref)
  • Enums
  • Arrays and nested objects
  • Required/optional properties

Example OpenAPI Spec

openapi: 3.0.3
info:
  title: My API
  version: 1.0.0

paths:
  /users/{id}:
    get:
      operationId: getUserById
      tags: [Users]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: User found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

components:
  schemas:
    User:
      type: object
      required: [id, name, email]
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string
          format: email
        createdAt:
          type: string
          format: date-time

    ApiError:
      type: object
      required: [message]
      properties:
        message:
          type: string
        code:
          type: string

Build Integration

MSBuild Target

Add to your .csproj to regenerate on build:

<Target Name="GenerateApiClient" BeforeTargets="BeforeCompile">
  <Exec Command="restclient-gen -i api.yaml -o Generated -n MyApi.Client" />
</Target>

Pre-build Event

Or use a pre-build event:

<PropertyGroup>
  <PreBuildEvent>
    restclient-gen -i $(ProjectDir)api.yaml -o $(ProjectDir)Generated -n MyApi.Client
  </PreBuildEvent>
</PropertyGroup>

Best Practices

  1. Version your OpenAPI specs alongside your code
  2. Regenerate clients when specs change
  3. Use tags to organize and filter operations
  4. Review generated code before committing
  5. Add generated files to .gitignore or commit them based on your workflow

Next Steps