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.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// 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
}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"
)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.
method:
oneOf:
- $ref: '#/components/schemas/CardPayment'
- $ref: '#/components/schemas/BankPayment'
discriminator:
propertyName: kind// 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 ""
}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
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)
}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."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.
.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.