OpenAPI·C#·Free

OpenAPI to
C#.

Turn an OpenAPI 3.0 or 3.1 spec into a modern C# client. Records with required and init, nullable reference types on every optional field, [JsonPolymorphic] + [JsonDerivedType] for each oneOf, and an HttpClient-based service. Pure System.Text.Json — no Newtonsoft, no extra runtime package. No signup, no install.

OpenAPI 3.0 / 3.1·C# 11+·.NET 8+·HttpClient·System.Text.Json·MIT
payments.openapi.yamlInput
openapi: 3.1.0
info:
  title: Payments API
  version: 1.2.0
paths:
  /payments/{id}:
    get:
      operationId: getPayment
      tags: [payments]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: expand
          in: query
          required: false
          schema: { type: string }
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Payment'
components:
  schemas:
    Payment:
      type: object
      required: [id, amount, currency, status, method]
      properties:
        id:          { type: string }
        amount:      { type: number, format: double }
        currency:    { $ref: '#/components/schemas/Currency' }
        status:      { $ref: '#/components/schemas/PaymentStatus' }
        description: { type: string }
        method:      { $ref: '#/components/schemas/PaymentMethod' }
    PaymentMethod:
      oneOf:
        - $ref: '#/components/schemas/CardPayment'
        - $ref: '#/components/schemas/BankPayment'
      discriminator:
        propertyName: kind
        mapping:
          card: '#/components/schemas/CardPayment'
          bank: '#/components/schemas/BankPayment'
compile
Models/Payment.cs tsc cleanOutput
// Models/Payment.cs
using System.Text.Json.Serialization;
using System;

namespace Example.Payments.Models;

public record Payment
{
    [JsonPropertyName("id")]
    public required string Id { get; init; }
    [JsonPropertyName("amount")]
    public required double Amount { get; init; }
    [JsonPropertyName("currency")]
    public required Currency Currency { get; init; }
    [JsonPropertyName("status")]
    public required PaymentStatus Status { get; init; }
    [JsonPropertyName("description")]
    public string? Description { get; init; }
    [JsonPropertyName("createdAt")]
    public required DateTime CreatedAt { get; init; }
    [JsonPropertyName("method")]
    public required PaymentMethodUnion Method { get; init; }
}
What gets generated

Records, a polymorphic union, an HttpClient.

Below: types emitted from a small payments spec with a oneOf + discriminator and an enum. Every schema becomes a record with init properties; required covers every required field; the oneOf union is an interface wired through [JsonPolymorphic] so System.Text.Json round-trips it without a custom converter. The discriminator mapping values land in [JsonDerivedType] — not the $ref'd schema names.

// Models/Payment.cs
using System.Text.Json.Serialization;
using System;

namespace Example.Payments.Models;

public record Payment
{
    [JsonPropertyName("id")]
    public required string Id { get; init; }
    [JsonPropertyName("amount")]
    public required double Amount { get; init; }
    [JsonPropertyName("currency")]
    public required Currency Currency { get; init; }
    [JsonPropertyName("description")]
    public string? Description { get; init; }
    [JsonPropertyName("createdAt")]
    public required DateTime CreatedAt { get; init; }
    [JsonPropertyName("method")]
    public required PaymentMethodUnion Method { get; init; }
}

// Models/PaymentMethodUnion.cs
using System.Text.Json.Serialization;

namespace Example.Payments.Models;

[JsonPolymorphic(TypeDiscriminatorPropertyName = "kind")]
[JsonDerivedType(typeof(CardPayment), "card")]
[JsonDerivedType(typeof(BankPayment), "bank")]
public interface PaymentMethodUnion
{
}

// Models/CardPayment.cs (BankPayment is the symmetric sibling)
public record CardPayment : PaymentMethodUnion
{
    [JsonPropertyName("kind")]
    public required string Kind { get; init; }
    [JsonPropertyName("last4")]
    public required string Last4 { get; init; }
}

// Exhaustive pattern match at the call site — no cast,
// no "which property is set?" guesswork:
string Label(Payment p) => p.Method switch
{
    CardPayment c => $"card {c.Last4}",
    BankPayment b => $"bank {b.Iban}",
    _             => throw new InvalidOperationException(),
};
Three things we get right

Grounded in how the spec reads.

Each claim is something you can verify by running the converter on your own spec. Click a card to see the spec fragment that triggered it and the C# that came out.

01ProofRecords with required and init — not partial classes with setters.Spec · InCode · Out
spec.yaml fragment
components:
  schemas:
    Payment:
      type: object
      required: [id, amount, status]
      properties:
        id:          { type: string }
        amount:      { type: number, format: double }
        status:      { $ref: '#/components/schemas/PaymentStatus' }
        description: { type: string }
Payment.cs
public record Payment
{
    [JsonPropertyName("id")]
    public required string Id { get; init; }
    [JsonPropertyName("amount")]
    public required double Amount { get; init; }
    [JsonPropertyName("status")]
    public required PaymentStatus Status { get; init; }
    [JsonPropertyName("description")]
    public string? Description { get; init; }
}

// - record, not partial class
// - init accessors, not setters
// - required on every schema-required field — the
//   compiler rejects an object initializer that
//   forgets one
// - [JsonPropertyName] pins the wire name, so the
//   C# property can be PascalCase while the JSON
//   stays the spec's camelCase
C# · HttpClient

A typed service, not a stub.

Operations group by OpenAPI tag — one <code>{Tag}Client</code> class per tag, all extending a shared BaseHttpClient that owns the HttpClient, the base URL, and the JsonSerializerOptions. Each method is async, takes a CancellationToken, returns the parsed body directly. Query parameters with required=false default to null and drop out of the URL when not passed. Path parameters interpolate inline. System.Text.Json serializes both sides.

Client/PaymentsClient.cs generated
// Client/PaymentsClient.cs
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using Example.Payments.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Example.Payments.Client;

public class PaymentsClient : BaseHttpClient
{
    public PaymentsClient(
        HttpClient httpClient,
        string baseUrl,
        JsonSerializerOptions? jsonOptions)
        : base(httpClient, baseUrl, jsonOptions) { }

    public async Task<Payment> GetPaymentAsync(
        string id,
        string? expand = null,
        CancellationToken cancellationToken = default)
    {
        var _queryParts = new List<string>();
        if (expand != null) _queryParts.Add($"{Uri.EscapeDataString("expand")}={Uri.EscapeDataString((expand?.ToString()) ?? "")}");
        var _qs = _queryParts.Count > 0 ? "?" + string.Join("&", _queryParts) : "";
        return await ExecuteAsync<Payment>(
            HttpMethod.Get,
            $"/payments/{id}{_qs}",
            cancellationToken);
    }

    public async Task<Payment> CreatePaymentAsync(
        CreatePaymentRequest requestBody,
        CancellationToken cancellationToken = default)
    {
        return await ExecuteAsync<CreatePaymentRequest, Payment>(
            HttpMethod.Post,
            $"/payments",
            requestBody,
            cancellationToken);
    }
}

// Client/BaseHttpClient.cs (shared, condensed)
public abstract class BaseHttpClient
{
    protected readonly HttpClient _httpClient;
    protected readonly string _baseUrl;
    protected readonly JsonSerializerOptions? _jsonOptions;

    public BaseHttpClient(
        HttpClient httpClient,
        string baseUrl,
        JsonSerializerOptions? jsonOptions = null)
    {
        _httpClient = httpClient;
        _baseUrl = baseUrl.TrimEnd('/');
        _jsonOptions = jsonOptions;
    }

    protected async Task<TResponse> ExecuteAsync<TResponse>(
        HttpMethod method, string path, CancellationToken cancellationToken)
    {
        var requestUrl = $"{_baseUrl}{path}";
        using var request = new HttpRequestMessage(method, requestUrl);
        using var response = await _httpClient
            .SendAsync(request, cancellationToken)
            .ConfigureAwait(false);
        response.EnsureSuccessStatusCode();
        var content = await response.Content
            .ReadAsStringAsync()
            .ConfigureAwait(false);
        return JsonSerializer.Deserialize<TResponse>(content, _jsonOptions)!;
    }

    // Sibling overloads handle request body / no response /
    // request body + no response. All use the same SendAsync +
    // EnsureSuccessStatusCode + Deserialize pipeline.
}
Also available via MCP

Or ask Claude to do it.

The same generator ships as a Model Context Protocol server. It calls the same web API this converter uses, so the output is identical. Point Claude Desktop, Cursor, or any MCP-aware agent at a spec and it produces the C# client as a diff — free, no token, no leaving the chat.

// claude-desktop config · .mcp.json
{
  "mcpServers": {
    "metaengine": {
      "command": "npx",
      "args": ["-y", "@metaengine/mcp-server"]
    }
  }
}

// Then in Claude:
// "Load specs/payments.yaml and generate a C# HttpClient
//  in namespace Acme.Payments."
See the MCP page
Prefer it in your build?

Same generator, as a package.

The converter above is the fastest way to try the output. For CI and repeatable builds, the identical generator ships as a package — pin a version, wire it into your pipeline, and review diffs like any other code.

Questions

Things people ask.

The questions that come up most when generating C# clients from OpenAPI. If yours is not here, run the converter — the answer is usually in the output.

Both OpenAPI 3.0 and 3.1. Beyond the shared subset (string enums, nullable: true on 3.0, path / query / header parameters, JSON request/response bodies, oneOf with discriminator, allOf inheritance), 3.1 documents get the full parser treatment: type: ["string","null"] nullability, numeric exclusiveMinimum/exclusiveMaximum values, const reified to single-value enums, contentEncoding/contentMediaType normalized to format: binary, top-level webhooks, RFC 6901 JSON Pointer escapes, and discriminator mappings preserved across multi-file specs.
More converters

Same engine, other stacks.

Every converter runs the same spec-to-IR pipeline. Pick another source format or target stack.

OpenAPITypeScript
Angular httpResource, React TanStack Query, or framework-agnostic fetch.
Open converter
OpenAPIAngular
Typed services with httpResource, Signals, inject() DI, and interceptors.
Open converter
OpenAPIReact
TanStack Query hooks with stable query keys, or plain async functions.
Open converter
OpenAPIFetch
Framework-agnostic fetch client with ApiResult, middleware, and retries.
Open converter
OpenAPIJava Spring
RestClient services, Java records, sealed interfaces, Spring Boot 3.
Open converter
OpenAPIPython
Async httpx clients paired with Pydantic v2 models.
Open converter
OpenAPIGo
Idiomatic net/http client, context-aware, pointer-nullable fields.
Open converter
OpenAPIKotlin
Ktor client, kotlinx.serialization, sealed interfaces for oneOf.
Open converter
OpenAPIRust
reqwest client with serde-tagged enums for oneOf and Option<T>.
Open converter
GraphQLAngular
Observable services with typed queries, mutations, and graphql-ws subscriptions.
Open converter
GraphQLReact
TanStack Query hooks with discriminated-union types and query keys.
Open converter
GraphQLKotlin
Ktor, kotlinx.serialization, Flow-based subscriptions.
Open converter
GraphQLC#
Records, nullable types, [JsonPolymorphic] unions, IAsyncEnumerable subscriptions.
Open converter
ProtobufKotlin
Ktor, coroutines, and sealed classes straight from .proto.
Open converter
ProtobufC#
Records, nullable types, Connect RPC over HttpClient.
Open converter
SQLTypeScript
Postgres DDL to typed interfaces with foreign-key navigation.
Open converter
SQLKotlin
CREATE TABLE scripts to Kotlin @Serializable data classes.
Open converter

Try it on your own spec.

The converter runs in the browser. Your spec never leaves the page.

Open the converter