OpenAPI·Go·Free

OpenAPI to
Go.

Turn an OpenAPI 3.0 or 3.1 spec into an idiomatic Go net/http client. Context on every method, pointer-optional fields, typed enums, and a discriminated-union struct for oneOf. Stdlib only — no runtime dependency. No signup, no install.

OpenAPI 3.0 + 3.1·net/http·context.Context·stdlib JSON·MIT
payments.openapi.yamlInput
openapi: 3.0.3
info:
  title: Payments API
  version: 1.2.0
paths:
  /payments/{id}:
    get:
      operationId: getPayment
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: expand
          in: query
          schema: { type: string }
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Payment'
components:
  schemas:
    Payment:
      type: object
      required: [id, amount, method]
      properties:
        id: { type: string }
        amount: { type: number, format: double }
        currency:
          type: string
          nullable: true
        method:
          oneOf:
            - $ref: '#/components/schemas/CardPayment'
            - $ref: '#/components/schemas/BankPayment'
          discriminator:
            propertyName: kind
compile
services/default_client.go tsc cleanOutput
// services/default_client.go
package services

import (
	"context"
	"fmt"
	"net/http"
	"net/url"

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

func (c *DefaultClient) GetPayment(ctx context.Context, id string, expand *string) (*models.Payment, error) {
	urlPath := fmt.Sprintf("/payments/%v", id)
	queryParams := url.Values{}
	if expand != nil {
		queryParams.Set("expand", fmt.Sprintf("%v", *expand))
	}
	if len(queryParams) > 0 {
		urlPath = urlPath + "?" + queryParams.Encode()
	}
	fullURL := c.baseURL + urlPath

	req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Accept", "application/json")

	var result models.Payment
	if err := doRequestJSON(c.httpClient, req, &result); err != nil {
		return nil, err
	}
	return &result, nil
}
What gets generated

Idiomatic Go. No client framework.

Below: types emitted from a small payments spec with an enum, a nullable field, and a oneOf. Each model lands in its own file; the union is a struct holding one pointer per variant, with custom JSON marshal/unmarshal so the wire shape stays flat.

// models/payment.go
package models

type Payment struct {
	ID       string        `json:"id"`
	Amount   float64       `json:"amount"`
	Currency *string       `json:"currency,omitempty"`
	Status   PaymentStatus `json:"status"`
	Method   MethodUnion   `json:"method"`
}

// models/union_types.go
package models

import (
	"encoding/json"
	"fmt"
)

// One pointer field per oneOf variant — exactly one is
// non-nil at a time. The Marshal/Unmarshal pair below
// keeps the wire shape flat: the variant is serialized
// directly, no envelope.
type MethodUnion struct {
	CardPayment *CardPayment
	BankPayment *BankPayment
}

func (u *MethodUnion) UnmarshalJSON(data []byte) error {
	var disc struct {
		Kind string `json:"kind"`
	}
	if err := json.Unmarshal(data, &disc); err != nil {
		return err
	}
	switch disc.Kind {
	case "CardPayment":
		u.CardPayment = &CardPayment{}
		return json.Unmarshal(data, u.CardPayment)
	case "BankPayment":
		u.BankPayment = &BankPayment{}
		return json.Unmarshal(data, u.BankPayment)
	default:
		return fmt.Errorf("unknown kind value: %s", disc.Kind)
	}
}

// models/payment_status.go
package models

type PaymentStatus string

const (
	PaymentStatusPending    PaymentStatus = "pending"
	PaymentStatusAuthorized PaymentStatus = "authorized"
	PaymentStatusCaptured   PaymentStatus = "captured"
	PaymentStatusRefunded   PaymentStatus = "refunded"
)
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 spec. Click a card for the spec fragment that triggered it and the Go code that came out.

01ProofoneOf as a discriminated union struct.Spec · InCode · Out
spec.yaml fragment
method:
  oneOf:
    - $ref: '#/components/schemas/CardPayment'
    - $ref: '#/components/schemas/BankPayment'
  discriminator:
    propertyName: kind
emitted .go + usage
// models/union_types.go
package models

import (
	"encoding/json"
	"fmt"
)

// One pointer per oneOf variant. Exactly one is non-nil.
type MethodUnion struct {
	CardPayment *CardPayment
	BankPayment *BankPayment
}

// UnmarshalJSON reads the discriminator first, then
// decodes into the matching variant. Wire shape stays
// flat — no envelope object around the variant.
func (u *MethodUnion) UnmarshalJSON(data []byte) error {
	var disc struct {
		Kind string `json:"kind"`
	}
	if err := json.Unmarshal(data, &disc); err != nil {
		return err
	}
	switch disc.Kind {
	case "CardPayment":
		u.CardPayment = &CardPayment{}
		return json.Unmarshal(data, u.CardPayment)
	case "BankPayment":
		u.BankPayment = &BankPayment{}
		return json.Unmarshal(data, u.BankPayment)
	default:
		return fmt.Errorf("unknown kind value: %s", disc.Kind)
	}
}

func (u MethodUnion) MarshalJSON() ([]byte, error) {
	if u.CardPayment != nil {
		return json.Marshal(u.CardPayment)
	}
	if u.BankPayment != nil {
		return json.Marshal(u.BankPayment)
	}
	return []byte("null"), nil
}

// Narrowing at the call site is a nil-check on each
// variant — the generated fields make every case visible:
func describe(m models.MethodUnion) string {
	switch {
	case m.CardPayment != nil:
		return "card " + m.CardPayment.Last4
	case m.BankPayment != nil:
		return "bank " + m.BankPayment.Iban
	}
	return ""
}
Go · net/http

A struct client. No framework.

The generator emits a DefaultClient struct holding *http.Client and the baseURL, plus methods that take context.Context first. A small api_helpers.go decodes JSON and wraps non-2xx responses. That is the entire surface — nothing else to learn.

services/default_client.go generated
// services/default_client.go
package services

import (
	"context"
	"fmt"
	"net/http"
	"net/url"

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

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

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

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

func (c *DefaultClient) GetPayment(ctx context.Context, id string, expand *string) (*models.Payment, error) {
	// ... builds urlPath, appends ?expand when non-nil
	req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Accept", "application/json")

	var result models.Payment
	if err := doRequestJSON(c.httpClient, req, &result); err != nil {
		return nil, err
	}
	return &result, nil
}

// services/api_helpers.go
package services

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

// doRequestJSON executes the HTTP request, checks the status code, and decodes
// the JSON response body into target. Works for both objects and slices.
func doRequestJSON(httpClient *http.Client, req *http.Request, target any) error {
	resp, err := httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to execute request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 400 {
		errBody, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(errBody))
	}

	return json.NewDecoder(resp.Body).Decode(target)
}
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 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 specs/payments.yaml and generate a Go net/http
//  client. Module github.com/acme/payments, package
//  payments, context-aware methods, stdlib JSON."
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.

A few recurring questions about the Go target. If yours is not here, run the converter — the answer is usually in the output.

A tree of .go files: one struct per schema under models/, typed enum constants, a discriminated-union struct per oneOf (with custom JSON marshal/unmarshal), and a services/ package with a struct client plus one method per operation. A small api_helpers.go holds shared JSON-decode and error-wrap logic.
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
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
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 spec.

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

Open the converter