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

Try it on your own spec.

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

Open the converter