Protobuf·C#·Free

Protobuf to
C#.

Turn a .proto file into a modern C# client. Records with required and init, nullable reference types on every optional, discriminated union records for each oneof. Ships with an HttpClient that speaks Connect RPC over JSON — unary plus server, client, and bidi streaming. No signup, no install.

proto3·C# 12·HttpClient·System.Text.Json·MIT
payments.protoInput
syntax = "proto3";

package payments.v1;

service PaymentsService {
  rpc GetPayment(GetPaymentRequest) returns (Payment);
  rpc CreatePayment(CreatePaymentRequest) returns (Payment);
}

message Payment {
  string id = 1;
  string customer_id = 2;
  int64 amount = 3;
  Currency currency = 4;
  PaymentStatus status = 5;
  optional string description = 6;
  oneof kind {
    CardPayment card = 10;
    BankPayment bank = 11;
  }
}

enum Currency {
  CURRENCY_UNSPECIFIED = 0;
  CURRENCY_EUR = 1;
  CURRENCY_USD = 2;
}
compile
Payment.cs tsc cleanOutput
// Models/Payment.cs
using System.Text.Json.Serialization;

namespace Example.Payments.Models;

public record Payment
{
    [JsonPropertyName("id")]
    public required string Id { get; init; }
    [JsonPropertyName("customerId")]
    public required string CustomerId { get; init; }
    [JsonPropertyName("amount")]
    public required long 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("kind")]
    public KindUnion? Kind { get; init; }
}
What gets generated

Idiomatic C#. Not a generated-sources folder.

Below: types emitted from a small payments .proto with a oneof, an optional, and an enum. Every message is a record with init properties; required covers every non-optional field; System.Text.Json attributes pin the JSON property names so the wire shape stays stable as the C# names evolve.

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

namespace Example.Payments.Models;

public record Payment
{
    [JsonPropertyName("id")]
    public required string Id { get; init; }
    [JsonPropertyName("customerId")]
    public required string CustomerId { get; init; }
    [JsonPropertyName("amount")]
    public required long Amount { get; init; }
    [JsonPropertyName("currency")]
    public required Currency Currency { get; init; }
    [JsonPropertyName("description")]
    public string? Description { get; init; }
    [JsonPropertyName("kind")]
    public KindUnion? Kind { get; init; }
}

// Models/Currency.cs
using System.Text.Json.Serialization;
using System.Runtime.Serialization;

namespace Example.Payments.Models;

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Currency
{
    [EnumMember(Value = "CURRENCY_UNSPECIFIED")]
    CurrencyUnspecified = 0,
    [EnumMember(Value = "CURRENCY_EUR")]
    CurrencyEur = 1,
    [EnumMember(Value = "CURRENCY_USD")]
    CurrencyUsd = 2
}

// Exhaustive pattern match — no cast, no HasX / ClearX dance:
string Label(Payment p) => p.Kind switch
{
    KindUnionCard c => $"card {c.Value.Last4}",
    KindUnionBank b => $"bank {b.Value.Iban}",
    null            => "no method",
    _               => throw new InvalidOperationException(),
};
Three things we get right

Grounded in how the proto reads.

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

01ProofRecords with required and init — not partial classes with setters.Spec · InCode · Out
payments.proto fragment
message Payment {
  string id = 1;
  string customer_id = 2;
  optional string description = 6;
  // ...
}
Payment.cs
public record Payment
{
    [JsonPropertyName("id")]
    public required string Id { get; init; }
    [JsonPropertyName("customerId")]
    public required string CustomerId { get; init; }
    [JsonPropertyName("description")]
    public string? Description { get; init; }
    // ...
}

// - record, not partial class
// - init accessors, not setters
// - required on every non-optional field, so the
//   compiler rejects construction without them
// - JSON property names are emitted via
//   [JsonPropertyName], camelCase in code
C# · HttpClient

A typed service, not a stub.

The service extends a thin BaseHttpClient that owns the HttpClient and the base URL. Each unary RPC is an async method that POSTs to /package.Service/Method with a Connect-Protocol-Version: 1 header and a JSON body. Server-stream RPCs return IAsyncEnumerable<T> and read NDJSON line-by-line from the response. Opt into Microsoft.Extensions.DependencyInjection wiring (HttpClientFactory + an AddPaymentsService extension) by toggling UseDependencyInjection. System.Text.Json handles both sides.

Client/PaymentsService.cs generated
// Client/PaymentsService.cs
using System.Net.Http;
using System.Text.Json;
using Example.Payments.Models;

namespace Example.Payments.Client;

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

    public async Task<Payment> GetPaymentAsync(
        GetPaymentRequest requestBody,
        CancellationToken cancellationToken = default)
    {
        return await ExecuteConnectAsync<Payment>(
            "/payments.v1.PaymentsService/GetPayment",
            requestBody,
            cancellationToken).ConfigureAwait(false);
    }

    public async Task<Payment> CreatePaymentAsync(
        CreatePaymentRequest requestBody,
        CancellationToken cancellationToken = default)
    {
        return await ExecuteConnectAsync<Payment>(
            "/payments.v1.PaymentsService/CreatePayment",
            requestBody,
            cancellationToken).ConfigureAwait(false);
    }
}

// Client/BaseHttpClient.cs (shared)
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> ExecuteConnectAsync<TResponse>(
        string path, object? body, CancellationToken cancellationToken)
    {
        var requestUrl = $"{_baseUrl}{path}";
        using var request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
        request.Content = new StringContent(
            body is null ? "{}" : JsonSerializer.Serialize(body, _jsonOptions),
            Encoding.UTF8,
            "application/json");
        request.Headers.TryAddWithoutValidation(
            "Connect-Protocol-Version", "1");
        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)!;
    }

    // For server-stream RPCs (`stream` return type),
    // a sibling helper returns IAsyncEnumerable<T> and
    // reads NDJSON line-by-line from the response body.
    protected async IAsyncEnumerable<TResponse> ExecuteServerStreamAsync<TResponse>(
        string path, object? body,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    { /* NDJSON loop over the response stream */ }
}
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 .proto 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 proto/payments.proto 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 we keep getting about Protobuf → C#. If yours is not here, run the converter — the answer is usually in the output.

No — it speaks Connect RPC over JSON. Each call POSTs to /package.Service/Method with a Connect-Protocol-Version: 1 header and a JSON body. It is wire-compatible with Connect RPC servers (Buf's stack, Connect-Go, connect-es) and with JSON-transcoded gRPC gateways. For HTTP/2 + binary framing, protoc --csharp_out with Grpc.Tools is the established path. This generator's angle is a different one: a modern C# client generated from .proto as an IDL, with JSON on the wire.
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
OpenAPIC#
Records, nullable reference types, JsonPolymorphic, HttpClient.
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
ProtobufGo
Plain structs with json tags, pointer presence, stdlib-only Connect client.
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 .proto.

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

Open the converter