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
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 != 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
}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"
)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
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 ""
}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"
"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)
}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 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.