Protobuf·Go·Free

Protobuf to
Go.

Turn a .proto file into plain Go. Messages are structs with exported fields and standard json: tags — copyable, buildable from a literal, and readable by encoding/json. Repeated messages land as []LineItem, oneof as a union struct, and the client is net/http speaking Connect RPC. Stdlib only. No signup, no install.

proto3·net/http·context.Context·stdlib JSON·MIT
payments.protoInput
syntax = "proto3";

package payments.v1;

import "google/protobuf/timestamp.proto";

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;
  google.protobuf.Timestamp created_at = 7;
  repeated LineItem items = 8;
  oneof kind {
    CardPayment card = 10;
    BankPayment bank = 11;
  }
}

enum Currency {
  CURRENCY_UNSPECIFIED = 0;
  CURRENCY_EUR = 1;
  CURRENCY_USD = 2;
}
compile
models/payment.go tsc cleanOutput
// models/payment.go
package models

import "time"

type Payment struct {
	ID          string        `json:"id"`
	CustomerID  string        `json:"customerId"`
	Amount      int64         `json:"amount"`
	Currency    Currency      `json:"currency"`
	Status      PaymentStatus `json:"status"`
	Description *string       `json:"description,omitempty"`
	CreatedAt   *time.Time    `json:"createdAt,omitempty"`
	Items       []LineItem    `json:"items"`
	Kind        *KindUnion    `json:"kind,omitempty"`
}
What gets generated

Structs, enums, and a client. That is the surface.

Below: types emitted from a small payments .proto with an enum, an optional, a repeated, a timestamp, and a oneof. Each message lands in its own file. google.protobuf.Timestamp maps to *time.Time, proto3 optional to a pointer with ,omitempty, and the oneof to a struct with one pointer per variant whose MarshalJSON writes the variant name as the key.

// models/payment.go
package models

import "time"

type Payment struct {
	ID          string        `json:"id"`
	CustomerID  string        `json:"customerId"`
	Amount      int64         `json:"amount"`
	Currency    Currency      `json:"currency"`
	Status      PaymentStatus `json:"status"`
	Description *string       `json:"description,omitempty"`
	CreatedAt   *time.Time    `json:"createdAt,omitempty"`
	Items       []LineItem    `json:"items"`
	Kind        *KindUnion    `json:"kind,omitempty"`
}

// models/line_item.go
package models

type LineItem struct {
	Sku    string `json:"sku"`
	Amount int64  `json:"amount"`
}

// models/currency.go
package models

type Currency string

const (
	CurrencyUnspecified Currency = "CURRENCY_UNSPECIFIED"
	CurrencyEur         Currency = "CURRENCY_EUR"
	CurrencyUsd         Currency = "CURRENCY_USD"
)

// models/kind_union.go — one pointer per oneof variant
package models

type KindUnion struct {
	Card *CardPayment `json:"-"`
	Bank *BankPayment `json:"-"`
}

func (u KindUnion) MarshalJSON() ([]byte, error) {
	if u.Card != nil {
		return json.Marshal(map[string]any{"card": u.Card})
	}
	if u.Bank != nil {
		return json.Marshal(map[string]any{"bank": u.Bank})
	}
	return []byte("null"), nil
}
// ... UnmarshalJSON reads the variant name back off the key
Three things the Go output gets right

Grounded in how Go code actually 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 Go that came out.

01ProofA message is a plain struct. Nothing else is in it.Spec · InCode · Out
payments.proto fragment
message Payment {
  string id = 1;
  string customer_id = 2;
  int64 amount = 3;
  Currency currency = 4;
  // ...
}
models/payment.go + usage
// models/payment.go — the whole file
package models

import "time"

type Payment struct {
	ID          string        `json:"id"`
	CustomerID  string        `json:"customerId"`
	Amount      int64         `json:"amount"`
	Currency    Currency      `json:"currency"`
	Status      PaymentStatus `json:"status"`
	Description *string       `json:"description,omitempty"`
	CreatedAt   *time.Time    `json:"createdAt,omitempty"`
	Items       []LineItem    `json:"items"`
	Kind        *KindUnion    `json:"kind,omitempty"`
}

// Exported fields, standard json tags, nothing else in
// the struct. So it composes like any other Go value:
p := models.Payment{
	ID:       "pay_123",
	Amount:   4999,
	Currency: models.CurrencyEur,
}
snapshot := p                      // ordinary value copy
b, err := json.Marshal(p)          // encoding/json, no extra import
Go · net/http

A struct client. No framework.

The generator emits one client struct per proto service, holding an *http.Client and the base URL, plus a method per RPC that takes context.Context first. A shared connect_call.go POSTs to /package.Service/Method with a Connect-Protocol-Version: 1 header. Errors come back through connect_error.go, which decodes the Connect error envelope into rpc error: code = ..., message = ... rather than routing on HTTP status. Server-streaming RPCs take a handler callback and client-streaming RPCs take an iterator-style producer, both over NDJSON.

services/payments_service_client.go generated
// services/payments_service_client.go
package services

import (
	"context"
	"net/http"

	"github.com/example/payments/models"
)

// PaymentsServiceClient is the HTTP client for this service.
type PaymentsServiceClient struct {
	httpClient *http.Client
	baseURL    string
}

// NewPaymentsServiceClient creates a new PaymentsServiceClient instance.
func NewPaymentsServiceClient(baseURL string) *PaymentsServiceClient {
	httpClient := &http.Client{}

	return &PaymentsServiceClient{
		httpClient: httpClient,
		baseURL:    baseURL,
	}
}

// GetPayment Get payment
func (c *PaymentsServiceClient) GetPayment(ctx context.Context, body *models.GetPaymentRequest) (*models.Payment, error) {
	var result models.Payment
	if err := connectUnary(ctx, c.httpClient, c.baseURL, "/payments.v1.PaymentsService/GetPayment", body, &result); err != nil {
		return nil, err
	}
	return &result, nil
}

// services/connect_call.go (shared)
package services

// connectUnary executes a Connect unary RPC. Marshals body, POSTs to path with
// the Connect headers, decodes the error envelope via decodeConnectError, and
// decodes the JSON response into target. Pass a nil target for RPCs that do
// not read the response body.
func connectUnary(ctx context.Context, httpClient *http.Client, baseURL, path string, body, target any) error {
	bodyBytes, err := json.Marshal(body)
	if err != nil {
		return fmt.Errorf("failed to marshal request: %w", err)
	}
	req, err := http.NewRequestWithContext(ctx, "POST", baseURL+path, bytes.NewReader(bodyBytes))
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Connect-Protocol-Version", "1")
	resp, err := httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()
	if err := decodeConnectError(resp); err != nil {
		return err
	}
	if target == nil {
		return nil
	}
	if err := json.NewDecoder(resp.Body).Decode(target); err != nil {
		return fmt.Errorf("failed to decode response: %w", err)
	}
	return nil
}

// services/connect_error.go — the Connect error envelope,
// decoded into a Go error:
//   rpc error: code = not_found, message = payment missing
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 Go 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 Go net/http
//  client. Module github.com/acme/payments, package
//  payments, context on every call."
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 → Go. If yours is not here, run the converter — the answer is usually in the output.

It speaks Connect RPC over JSON. Each call POSTs to /package.Service/Method with a Connect-Protocol-Version: 1 header and a JSON body, which is wire-compatible with Connect RPC servers (connect-go, connect-es). For binary gRPC over HTTP/2, protoc-gen-go plus protoc-gen-go-grpc is the established path. This generator is built for a different shape: .proto as the IDL, JSON on the wire, and a client that is ordinary net/http.
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
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 .proto.

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

Open the converter