OpenAPI·Go·Free

OpenAPI to
Go.

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

OpenAPI 3.0·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 != "" {
		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 sealed interface that only the oneOf members can satisfy.

// 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

// Sealed interface — only CardPayment and BankPayment
// satisfy it, because isMethodUnion is unexported.
type MethodUnion interface{ isMethodUnion() }

func (*CardPayment) isMethodUnion() {}
func (*BankPayment) isMethodUnion() {}

// 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 sealed interface.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

// Sealed interface: the marker method is unexported,
// so only types in this package can satisfy it.
type MethodUnion interface{ isMethodUnion() }

func (*CardPayment) isMethodUnion() {}
func (*BankPayment) isMethodUnion() {}

// Narrowing is a type switch — yours to write,
// the generated types make it exhaustive:
func describe(m models.MethodUnion) string {
	switch v := m.(type) {
	case *models.CardPayment:
		return "card " + v.Last4
	case *models.BankPayment:
		return "bank " + v.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"

	"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 {
	return &DefaultClient{
		httpClient: &http.Client{},
		baseURL:    baseURL,
	}
}

func (c *DefaultClient) GetPayment(
	ctx context.Context,
	id string,
	expand string,
) (*models.Payment, error) {
	// ... builds URL, appends ?expand when non-empty
	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 request, checks the status,
// and decodes the JSON body into target.
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
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 sealed interface per oneOf, 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.

Try it on your own spec.

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

Open the converter