Generated SDKs
Generate a native typed client for your API in TypeScript, Python, or Go.
Typeship generates native TypeScript, Python, and Go SDKs for your API. Each package includes typed requests and responses, documented errors, pagination, retries, hooks, and optional runtime validation.
Choose any language your users need. Each SDK is an independent package and release stream; generating a CLI or MCP server never creates a public SDK implicitly.
Use this reference for shared client behavior and select your language in the examples. The language pages cover additional TypeScript, Python, and Go options. To generate and install a package first, follow the Quickstart.
The examples below use a fictional Parcel API and a locally generated parcel-client package. They illustrate client behavior; the package and endpoints are not published tutorial services. Use the names and operations in your generated README.
What ships in the package
parcel-client/
package.json zero runtime dependencies, ESM, Node 20+
src/index.ts ParcelClient, ClientOptions, environments, VERSION
src/types.ts every schema as an interface or type
src/errors.ts status family and documented-status error classes, per-operation unions
src/resources/*.ts one module per resource
src/core/ HTTP runtime and pagination
src/webhooks.ts when the spec declares webhooks
src/schemas.ts validation tables, plain data
tests/*.test.mjs one smoke test per operation, node:test only
api.md the full surface reference in one file
api.json machine-readable operations, schemas, examples, safety
AGENTS.md context for coding agents
PUBLISHING.md build, publish, and customization steps for the publisherThe package ships as readable TypeScript source and compiles with npm run build. main, types, and exports point into dist/.
Forward-compatible response types
Request types stay strict: the SDK should stop a caller from sending a value the Spec does not allow. Response types are more tolerant because servers can add enum values, discriminator variants, and fields before every consumer upgrades.
- When one component is meaningfully different on the wire, generated models split into directional names such as
ShipmentWritefor requests andShipmentReadfor responses. NestedreadOnly,writeOnly, enums, and discriminators participate in that decision; Typeship does not duplicate models whose shapes are actually identical. - Response enums keep autocomplete for documented values while accepting an unknown string. Request enums remain closed.
- OpenAPI tagged unions include an unknown-object fallback on responses. In TypeScript, comparing the tag to a documented value, such as
if (event.type === "shipment.delivered"), narrows to that member. A member added later reaches the fallback branch as anUnknownVariant; read its tag withString(event.type). GraphQL unions remain closed over the schema revision because__typenameselection is explicit; the raw GraphQL selection and response remain the escape hatch. - TypeScript response metadata preserves
rawBody; Go'sAPIResponse.Bodyand unionRaw()preserve bytes. Python response validation accepts future enum and discriminator values instead of rejecting a successful server response.
These rules permit additive server changes while retaining strict request types. Removing a documented response variant or widening a request remains visible in compatibility review.
Create a client
import { ParcelClient } from "parcel-client";
const client = new ParcelClient({
bearerToken: process.env.PARCEL_TOKEN!,
});The TypeScript client does not read credentials from the environment. Pass them explicitly. This keeps the client safe in browsers and edge runtimes, where process.env may not exist. The generated CLI and MCP server are the pieces that read PARCEL_TOKEN.
Client options
Every option is optional when the spec pins a server URL. If it does not, baseUrl becomes a required argument and generation warns you.
const client = new ParcelClient({
baseUrl: "https://api.parcel.example/v1", // defaults to the spec's first server
bearerToken: process.env.PARCEL_TOKEN!,
timeoutMs: 60_000, // per attempt
maxRetries: 2, // retries after the first attempt
defaultHeaders: { "Request-Source": "billing" },
fetch: myFetch, // bring your own: proxies, tests, instrumentation
onRequest: (context) => {},
onResponse: (response, context) => {},
onError: (error, request) => {},
debug: false,
validate: false,
});Authentication
For shared API settings, OAuth applications, and each Target's responsibilities, see Authentication.
Which auth options exist depends on the security schemes in your spec.
| Spec declares | TypeScript | Python | Go |
|---|---|---|---|
| HTTP bearer, OAuth2, OpenID Connect | bearerToken | bearer_token | WithBearerToken, WithBearerTokenFunc, WithBearerTokenFuncContext |
| One API key header | apiKey | api_key | WithAPIKey |
| Several API keys | one option per header or query param | one keyword per param | one With... per param |
| HTTP basic | basicAuth: { username, password } | username, password | WithBasicAuth |
| OAuth2 client credentials | clientCredentials | client_credentials | WithClientCredentials |
Bearer tokens and API keys accept a callback, resolved before every attempt. Use it for credentials that expire:
const client = new ParcelClient({
bearerToken: async () => (await tokenManager.current()).accessToken,
});In Python, the asynchronous client accepts async def callbacks as well as synchronous ones; the synchronous client accepts synchronous callbacks only. When a Python request sent with a callback or client-credentials token receives 401, the client discards the cached token, resolves the credential again, and resends once without using the retry budget. Static credentials are never resent, and an exception raised by a callback propagates unchanged. Python's client.with_credentials(...) returns a client that sends only the credentials you pass, sharing the original client's other settings and connections. See Python.
In Go, pass a bearer callback with WithBearerTokenFunc and an API-key callback with WithCredentialFunc(schemeName, callback). Their Context variants, WithBearerTokenFuncContext and WithCredentialFuncContext, receive the request's context so a token lookup can honor its deadline and cancellation. A callback error is returned unchanged and is not retried. When a Go request authenticated by a callback or client-credentials token receives 401, the client discards any cached token and sends the request once more, without using a retry. Static credentials are not resent. See Go: Choose credentials for each endpoint.
When the Spec or shared authentication configuration enables OAuth2, clientCredentials fetches and caches access tokens for a server application. The client posts to the token URL of the spec's clientCredentials flow, or your configured token URL, caches the token until a minute before expiry, and shares one in-flight token request across concurrent calls. An explicit bearer token always wins.
const client = new ParcelClient({
clientCredentials: {
clientId: process.env.PARCEL_CLIENT_ID!,
clientSecret: process.env.PARCEL_CLIENT_SECRET!,
scopes: ["read:shipments"], // optional
tokenParams: { audience: "https://api.parcel.example" }, // if your auth server needs one
authMethod: "post", // or "basic"
tokenUrl: "https://auth.parcel.example/oauth/token", // required only if the spec has none
},
});Python takes client_credentials={"client_secret": ...} and Go takes WithClientCredentials(parcel.ClientCredentials{ClientSecret: ...}) when the selected OAuth application supplies the client ID. Otherwise, provide client_id or ClientID too. Both cache and coordinate token requests. In Go, the token request uses the calling request's context.
A selected OAuth application supplies the client ID and client authentication method; the shared OAuth server supplies the token endpoint, scopes, audience, and resource. Runtime options take precedence. With these defaults configured, TypeScript only needs clientCredentials: { clientSecret: process.env.PARCEL_CLIENT_SECRET! }. Keep this secret in your server application, never browser code or generated configuration. See Configure a machine client.
A server that calls your API for many users can create one client and derive a copy per user. In TypeScript, withCredentials returns a client with the same configuration and only the credentials you pass; nothing is inherited from the original client:
const client = new ParcelClient({ timeoutMs: 10_000 });
const forUser = client.withCredentials({ bearerToken: user.accessToken });
await forUser.shipments.list();When the API returns 401 for a request that used a callback or a clientCredentials token, the TypeScript client drops the cached token, resolves the credential again, and resends once. A static credential is not resent. An error thrown by your credential callback is thrown unchanged and is not retried.
Environments
When the spec declares two or more servers, the SDK exports them by name, taken from each server's description. The first server stays the default.
import { ParcelClient, environments } from "parcel-client";
const client = new ParcelClient({ baseUrl: environments.sandbox, bearerToken: token });Call an operation
Operations are grouped into resources by their first tag. Path parameters are positional, in path order. The body comes next, then query and header parameters, then per-call options.
await client.shipments.get("shp_123");
await client.shipments.create({ recipient: "Ada Lovelace", service: "ground" });
await client.shipments.list({ limit: 50, created: { gte: 1700000000 } });Method names are de-stuttered. An operationId of createShipment on the shipments resource becomes client.shipments.create. The type names follow: ShipmentsCreateParams, ShipmentsCreateError.
Per-call options
The last argument of every method overrides the client for that one call.
await client.shipments.get("shp_123", {
timeoutMs: 5_000,
maxRetries: 0,
headers: { "Request-Source": "cron" },
signal: controller.signal, // composed with the per-attempt timeout
});Precedence is the same everywhere: per-call, then per-operation policy set at generation, then the client.
Results and errors
Awaiting a call returns the response data. Failures throw typed errors, including failures while fetching any page of a list. There is no generated non-throwing SDK variant.
import { ParcelClient, NotFoundError, ResponseParseError } from "parcel-client";
try {
const shipment = await client.shipments.get("shp_123");
console.log(shipment.id);
} catch (error) {
if (error instanceof NotFoundError) {
console.error(error.code, error.body, error.requestId);
} else if (error instanceof ResponseParseError) {
console.error(error.body); // malformed successful JSON as text
}
throw error;
}Errors share code, status, requestId, body, and an actionable message. The message leads with the status and the API's own message, such as HTTP 404: No such shipment. code is the API's error code when the body has one (error.code, code, error.type, or errors[0].code), else http_<status>. A missing HTTP response has status: null and no request ID or API body. The error classes are:
- One class per status family, raised whether or not the operation documents the status:
BadRequestError(400),UnauthorizedError(401),ForbiddenError(403),NotFoundError(404),ConflictError(409),UnprocessableEntityError(422),RateLimitError(429), andServerError(any 5xx). Each extendsApiError, andbodyis typed where the operation documents the response. - Classes for other statuses the spec documents, such as
GoneErrorfor a410. NotModifiedErrorfor a304 Not Modified, which arrives only when you send a conditional header. See Conditional requests.UnexpectedApiErrorfor any other status the spec did not document.RateLimitErroralso covers a rate limit that arrives as a403. See Rate limits.PayloadErrorwhen a2xxbody reports a failure. See Failures inside successful responses.ResponseParseErrorwhen a successful response declares JSON but carries a malformed body. It preserves the HTTP status, raw text body, and response metadata.TransportErrorwhen the request fails before a complete response arrives: network failure, timeout, abort, or a truncated body. In TypeScript itscodesays which:abortedwhen yourAbortSignalcancelled the request,timeoutwhen the per-attempt timeout elapsed, andtransport_errorotherwise.ValidationErrorwhen runtime validation is on and a body fails its schema.
Failures inside successful responses
Some APIs report a failure in a 2xx body. The SDK raises these instead of returning them as data:
- A success envelope whose
okorsuccessflag isfalse, when the spec declares that flag as alwaystrue(enum: [true]) or declares anerrororerrorsfield beside it. Slack's{"ok": false, "error": "invalid_auth"}and Cloudflare's{"success": false, "errors": [...]}both qualify. Other booleansuccessfields stay ordinary data. - A GraphQL result that resolves to an error type of a union or interface, such as
UserErrororAccessDeniedError. Members whose names end inErrorcount, as long as the union also has a non-error member. The SDK always selects__typenameon these fields so it can tell.
These raise PayloadError (*PayloadError in Go), an ApiError with the HTTP status and the failure body. Its code comes from the body (invalid_auth, 10000), and GraphQL failures carry the error type in typename (Typename in Go). The check runs before runtime validation, so a failure body is reported as the failure rather than as a schema mismatch.
Rate limits
A rate-limited call raises RateLimitError, whether it arrives as a 429 or as a 403 whose headers say the limit is spent. Every API error carries rateLimit (rate_limit in Python, RateLimit in Go) when the response was a rate limit, with retryAt set to when the limit resets if the API said. The message names that time, for example Rate limited: wait until 2026-09-26T16:04:12Z, then retry. The generated CLI and MCP server report these as RATE_LIMITED with the same time.
Conditional requests
Send If-None-Match or If-Modified-Since with a per-call header to ask whether a resource changed since your last read. When it has not, the API answers 304 Not Modified with no body, and the SDK raises NotModifiedError (*NotModifiedError in Go). A 304 arrives only when you send one of these headers, so methods keep their plain return type: Promise<Shipment> in TypeScript, Shipment in Python, and *Shipment in Go, never null, None, or a nil value with a nil error. A response body that the spec declares nullable stays nullable.
NotModifiedError is an ApiError with status 304 and code not_modified. It carries the validators to send next time: etag and lastModified in TypeScript, etag, last_modified, and headers in Python, and ETag(), LastModified(), and Header in Go. Catch it to keep using the copy you already have:
import { NotModifiedError } from "parcel-client";
let etag: string | undefined;
let shipment = await client.shipments.get("shp_123", { onResponse: (response) => { etag = response.etag; } });
try {
shipment = await client.shipments.get("shp_123", { headers: { "If-None-Match": etag } });
} catch (error) {
if (!(error instanceof NotModifiedError)) throw error;
// unchanged since the first call; keep using shipment
}onResponse receives the call's response metadata, including for a 304: status, headers, requestId, etag, lastModified, notModified, and rateLimit.
List methods behave the same way: a 304 raises instead of yielding an empty page. The generated CLI and MCP tools report a 304 as a successful result, {"ok": true, "not_modified": true, "etag": "..."}, with etag present when the API sent one.
Pagination
List operations with a recognized pagination shape iterate every item across every page, fetching lazily.
Paginated methods return a PagePromise. It is both awaitable and async-iterable:
// for await -> every item from every page
for await (const shipment of client.shipments.list({ limit: 50 })) {
console.log(shipment.id);
}
// await -> one page
const page = await client.shipments.list({ limit: 50 });
page.items; // Shipment[]
page.body; // the raw envelope
page.hasNextPage(); // boolean
await page.getNextPage(); // next Page, or null on the last oneAwaiting the first page, fetching the next page, and iterating all throw the operation's typed error when a page fetch fails.
These pagination styles are detected from the spec, in this order of preference:
| Style | Detected from |
|---|---|
| Next URL | A string field in the response that holds the next page's URL (next_page_uri, next_url, or next described or formatted as a URL), at the top level or inside a container such as links. The SDK requests that URL as given. |
| Link header | A declared Link response header, on a bare array response or an envelope. The SDK follows the rel="next" URL as given. |
cursor | A cursor query param (cursor, page_token, after, starting_after, ...) plus a next-cursor field in the response (next_cursor, next_page_token, ...), at the top level or one level down (meta.next_cursor, response_metadata.next_cursor, result_info.cursor) |
cursor_from_last_id | A cursor param plus a has_more field, where items have an id. The next request cursors from the last item's id. |
page | A page or page_number query param. Advances while pages look full. A page param with minimum: 0 starts at page 0. |
offset | An offset, skip, or start query param. Advances by the number of items received. |
Response envelopes built with allOf are merged before detection, so a shared envelope plus a result array (Cloudflare) is recognized, as is an item array one level inside result or data. A response whose only page of results sits under a named key ({"categories": {"items": [...], "next": "..."}}) pages through that key. A response with several such envelopes (a search returning tracks and artists) is not paginated automatically; generation warns and names them, and a pagination rule with an items field such as tracks.items picks one. A lone array next to another object, such as a lookup that returns a file and its comments, is not treated as a page of results.
Iteration stops when the API signals it: has_more: false, an empty next cursor or next URL, no rel="next" link, a total_count (or total) reached, a total_pages reached, a page shorter than the requested limit, or an empty page.
For these URL styles, nextPageParams() returns { page_url }, the next page's URL as the API named it. Pass it back as the pageUrl request option (TypeScript) to start a list from that page. The generated CLI's --page-url flag and MCP's page_url argument do the same.
A next page whose URL is on another origin is not requested, because the request carries your credentials; iteration fails with PaginationError, whose code is pagination_error in TypeScript, Python, and Go. A page that is missing its items field also fails with PaginationError instead of ending the walk as if the list were empty.
If detection does not match your API, a Project's configuration can pin the style, items field, and cursor fields per operation, or turn pagination off for one. Malformed rules fall back to detection with a warning on the generation.
Retries and timeouts
- Requests with idempotent verbs (GET, HEAD, PUT, DELETE, OPTIONS) retry on
408,429,500,502,503, and504, and on transport failures. 429is retried for every verb, including POST.- A
403that says the rate limit is spent (x-ratelimit-remaining: 0) or asks you to wait (Retry-After) is a rate limit, not a permission error. It is retried like other retryable statuses. - Backoff is exponential with full jitter, starting at 300ms and capped at 10 seconds per wait. When the response says how long to wait, through
Retry-After(seconds or HTTP date) orx-ratelimit-reset(epoch seconds or seconds until reset), the SDK waits exactly that long. A requested wait longer than 60 seconds is not slept through: the call fails at once with the reset time. Change the ceiling withmaxRetryWaitMs(TypeScript),max_retry_waitin seconds (Python), orWithMaxRetryWait(Go). - Defaults are two retries after the first attempt and a 60 second timeout per attempt. Override the timeout on the client or on a single call.
The policy is tunable per project in config: replace the retryable status set, change the retry count and backoff window, allow retries on non-idempotent operations, or disable retries globally or per operation.
Idempotency keys
When an operation declares an Idempotency-Key header, the SDK generates a UUID for it on every call you do not supply one for, and reuses that UUID across retries of the call. That is what makes retried writes safe. Pass your own through the params object (idempotencyKey in TypeScript, idempotency_key in Python, IdempotencyKey in Go) to control it.
Hooks and debug logging
Two hooks run on every attempt. onRequest runs after auth and body headers are assembled and may mutate headers or url. Python's on_request(method, url, headers) can change headers but not the URL. onResponse runs after every HTTP response, before parsing and retry decisions. onError runs once per failed call, after retries, with the typed error and the request method and path.
const client = new ParcelClient({
bearerToken: token,
onRequest: (context) => {
context.headers["Trace-Id"] = crypto.randomUUID();
},
onResponse: (response, context) => {
metrics.observe(context.method, response.status, context.attempt);
},
onError: (error, request) => {
logger.warn({ error, ...request });
},
});debug: true (or PARCEL_DEBUG=1 in the environment) logs one line per attempt to stderr: parcel POST /shipments -> 201 (43ms) req_8f2k1. Pass a function instead to feed your own logger a structured event with method, path, status, durationMs, attempt, requestId, and the transport error message when there was no response.
Debug events never include headers or bodies, so credentials cannot reach logs through them. Use the hooks when you need request-level detail.
Every generated SDK request carries a User-Agent such as parcel/2.3.0, identifying the package and version without a generator marker. Your API can use it to distinguish SDK versions. Override the header with TypeScript defaultHeaders, Python default_headers, or Go WithRequestHeader for a call. The packages also expose their version.
Runtime validation
Types catch mistakes at compile time. They cannot see an API that drifted from its spec at runtime. Turn on validation to schema-check JSON request and response bodies against your spec's own schemas, still with zero dependencies. The validator ships in the package, and the schema table is plain data generated from the spec.
const client = new ParcelClient({ validate: true });
try {
await client.shipments.get("shp_123");
} catch (error) {
if (error instanceof ValidationError) console.error(error.violations);
}Validation failure throws ValidationError. Use validate: { mode: "warn" } to log with console.warn and proceed, and requests / responses to toggle each direction.
Bad request bodies are caught before any HTTP is sent. The checks honor readOnly, writeOnly, and nullability. Constraints outside the emitted subset (format, multipleOf, and similar) are ignored, so validation can miss drift but never rejects valid traffic. All three languages validate against identical tables. GraphQL and streaming operations are not validated.
Generation warns about encountered unsupported constraints such as unevaluatedProperties, multipleOf, and conditional schemas. Add application checks for those constraints and for business rules expressed only in descriptions. Generated CLIs also validate path, query, and header parameters when --validate is enabled.
Webhooks
When your spec declares a webhooks section (OpenAPI 3.1, or x-webhooks on 3.0), the package ships typed events and a verifying parser. Verification follows the Standard Webhooks convention: webhook-id, webhook-timestamp, and webhook-signature headers, HMAC-SHA256, a five minute tolerance, and a constant-time compare.
const client = new ParcelClient({ webhookKey: process.env.PARCEL_WEBHOOK_KEY });
// in your webhook handler:
const event = await client.webhooks.unwrap(rawBody, req.headers);
switch (event.type) {
case "shipment.updated": event.shipment; // fully typed per event
case "shipment.delivered": event.shipment_id;
}unwrap throws WebhookVerificationError on any mismatch. unwrapUnsafe parses without verifying. See Webhooks for the end-to-end flow, including local testing with your CLI.
Streaming
Operations whose success response is text/event-stream return a stream of events instead of a parsed body. Each event has data, plus event and id when the server sends them.
const stream = await client.events.stream();
for await (const event of stream) {
event.data; // string; JSON.parse it if your API sends JSON
}The per-attempt timeout guards the connection and headers only, so long streams are not cut off. Once events are flowing nothing is retried, because replaying a dropped connection would redeliver events you already handled. A failure to connect surfaces before the first event.
Operations that stream on request
Some operations answer JSON by default and text/event-stream when the request asks for it, through a boolean stream field or a stream_format field that accepts sse. These operations, including multipart/form-data uploads such as transcriptions, get a second method whose name ends in Stream (_stream in Python). It sends the field for you (as a form part for a multipart upload), so its body type leaves the field out, and it yields each event's data parsed into the event schema from the spec. A data: [DONE] line ends the stream. An error event (event: error, or data with an error.message) ends it with an error whose code is the API's error code.
for await (const chunk of await client.chat.createCompletionStream({ model, messages })) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}The JSON method keeps its JSON return type. If you set the stream field on it anyway, the call fails with unexpected_stream instead of returning event text as if it were JSON.
The generated CLI also exposes streaming operations and writes one JSON value per event as NDJSON. MCP servers omit them from the tool surface.
Global parameters
APIs where every call carries the same tenant or version parameter should not make callers repeat it. Naming those parameters in a project's config promotes them to client options: set once, applied to every operation that accepts them, with per-call values winning.
// generated with globals: ["account_id", "api-version"]
const client = new ParcelClient({ accountId: "acct_123", apiVersion: "2026-08" });
await client.reports.list(); // both applied
await client.reports.list({ accountId: "acct_9" }); // per-call value winsThe generated CLI and MCP server read the same values from PARCEL_ACCOUNT_ID-style environment variables. Query and header parameters are supported. Path parameters are not.
Types
Enums
Named string enums generate a runtime value and a type with the same name, so values exist at runtime and the type stays a literal union:
export const ShipmentStatus = { PENDING: "pending", IN_TRANSIT: "in_transit", DELIVERED: "delivered" } as const;
export type ShipmentStatus = (typeof ShipmentStatus)[keyof typeof ShipmentStatus];
client.shipments.update("shp_1", { status: ShipmentStatus.IN_TRANSIT });Python emits Literal["pending", "in_transit", "delivered"]. Go emits a defined type with ShipmentStatusPending-style constants.
An inline enum, written into a property or a parameter rather than declared as its own schema, stays a type-only literal union in TypeScript and a Literal[...] in Python. Go has neither, so it gets a defined type as well, named after the field it belongs to (SourceKind, GenerationMetaBaseline). When the same set of values turns up in more than one place, it is a concept the API repeats rather than a detail of one field, so it takes the concept's own name and every site shares the one type:
type Platform string
const (
PlatformSDK Platform = "sdk"
PlatformCLI Platform = "cli"
PlatformMCP Platform = "mcp"
)A defined string type marshals as the string it is, so nothing about the request or response bytes changes, and a value the SDK predates still decodes rather than failing the call.
readOnly and writeOnly
Properties marked readOnly are omitted from request types, so the compiler stops you sending id or created_at fields the server owns. writeOnly properties are omitted from response types. In TypeScript a referenced schema appears as Omit<Shipment, "id" | "tracking_number"> in request positions. Python and Go materialize a named variant only when the filter removes something: ShipmentWrite in Python, ShipmentParams in Go. The canonical type keeps every property.
Union types
oneOf and anyOf become TypeScript unions and Python Union[...]. Go has no sum types, so they become a named union type holding the raw JSON with As<Variant>()/From<Variant>() accessors and a Discriminator(), never a struct that would silently drop fields. See Go: Unions.
Deep bracket encoding
Query objects and form-encoded bodies use bracket-style deep encoding:
{ created: { gte: 5 } } -> created[gte]=5
{ items: [{ id: "x" }] } -> items[0][id]=x
{ expand: ["a", "b"] } -> expand=a&expand=b (top-level arrays repeat the key)
new Date(...) -> ISO 8601 stringJSON bodies are sent as JSON. This encoding applies to query strings and application/x-www-form-urlencoded bodies. A query array declared with explode: false, spaceDelimited, or pipeDelimited is joined into one value instead, such as expand=a,b. See Query parameters.
Form bodies are identical byte for byte across the three SDKs: pairs are ordered by key, and only letters, digits, -, _, ., and ~ stay unescaped (a space is +). A form field whose encoding says explode: false, spaceDelimited, or pipeDelimited is joined into one value the same way a query array is.
When a request body offers several media types, the SDK sends one: multipart/form-data when it carries files, else JSON, else form fields, else the first listed. Generation warns about every operation whose other variants were dropped.
GraphQL
Clients generated from a GraphQL schema put queries on client.query and mutations on client.mutation (client.Query / client.Mutation in Go). Generated methods select all scalar and enum fields to depth 2 by default, with a fragment per concrete type for unions and interfaces, and every object-returning method takes an optional selection that replaces the default: a typed select object in TypeScript that narrows the result type, select= in Python, WithSelection(...) in Go. The endpoint and authentication scheme come from the GraphQL Spec settings, since SDL cannot declare them. Relay-style connections auto-paginate. Root fields whose names start with an underscore, such as _dummy or _service, are placeholder or gateway fields and are not generated. In-band errors surface as GraphQLRequestError (*GraphQLError in Go). See Generate from GraphQL.
Naming rules
- Operations group into resources by their first tag. Untagged operations group by the first meaningful path segment after a versioned prefix such as
/platform/v1;/apiand/restare also skipped. A resource before a path parameter, such asshipmentsin/shipments/{id}/v1/events, remains the owner. - A meaningful
operationIdbecomes the method name. Framework suffixes such asUsingGETand controller namespaces such asShipmentsController.orShipmentsController_are stripped. Other PascalCase namespaces are stripped only when they end in a resource or route prefix and introduce an action verb. - A controller's
findAllorfindOnebecomeslistorgetwhen the GET route agrees. Custom actions remain intact. A controller whose resource differs from an authored tag keeps its namespace, even when it is the tag's only operation. Other ambiguous shortened names also retain their namespace. - An
operationIdthat merely restates method and path (GetShipmentsShipment) is treated as absent, and a name is derived by verb and path shape:list,get,create,update,delete.GET /shipmentsbecomesclient.shipments.list().GET /shipments/{id}becomesclient.shipments.get(id). - An
operationIdthat restates a path outside its tag's own resource is kept:getQuotesonGET /quotesunder theshipmentstag staysclient.shipments.getQuotes()instead of becoming a barelist. - Well-known trailing action segments win over the generic verb:
POST /shipments/{id}/cancelbecomesclient.shipments.cancel(id). - When a tag names only the last words of the noun every operation under it spells out, and the extra words are path segments, the resource takes the whole noun:
createInternationalLabelandlistInternationalLabelson/international/labelsunder aLabelstag becomeinternationalLabels.createandinternationalLabels.list. When every operation under a tag acts on the same noun from its path, the noun joins the resource:createTrackingEventandlistTrackingEventson/tracking/eventsunder aTrackingtag becometrackingEvents.createandtrackingEvents.list. - An
operationIdprefixedbeta_,alpha_, orexperimental_groups into its own resource:beta_createShipmentundershipmentsbecomesbetaShipments.create. - Resource words are trimmed from method names when the short form is unambiguous:
shipments.createShipmentbecomesshipments.create. A method name that only repeats its resource takes the verb for its HTTP method:GET /searchwithoperationId: searchbecomessearch.list. - Schema names that would shadow a language global or a runtime export get a
Modelsuffix: a schema namedErrorbecomesErrorModel. - Go identifiers follow Go's initialism convention:
UserID,HTTPStatus.
These rules also determine generated CLI commands and MCP tool names. Regeneration can rename existing methods, commands, and tools when their source uses framework namespaces or versioned prefixes. Check the generated reference and update callers for those renames; command-shortening aliases do not preserve names from an earlier Generation. See compatibility review.