← Docs

Response content types

How the generated TypeScript client types and reads a response whose success status declares more than one content type.

An OpenAPI operation can declare more than one content type for a single success response, or a different content type on each status code — a download endpoint returning either a JSON error summary or the raw file, for example. The generated client types the reply as the union of every declared variant, and reads the response body by the Content-Type header it actually receives, not by the status code or by guessing.

Classification

Every success response's content map is classified per media type, across every success status the operation declares:

  • A JSON media type (application/json, text/json, or any +json suffix) classifies as json and is modeled — the reply type carries your schema.
  • A textual type (text/*, or a handful of textual application/* types such as application/xml) classifies as text and reads as a plain string.
  • Anything else classifies as binary and reads as a Blob.

An operation with exactly one variant is unaffected: its reply type and generated code are exactly what they were before content types were classified at all. An operation with more than one variant gets a union reply type — but not only of the variants it declares. The runtime dispatch below can hand back Blob or string for a Content-Type no declared variant named, so a multi-variant reply type always includes both, whatever subset of json/text/binary the operation actually declares:

// application/json and application/octet-stream both declared on 200
async function downloadReport(context: DownloadReportContext): Promise<HttpClientResponse<Report | string | Blob>>

// application/json on 200, image/png on 202
async function getRenderedReport(context: GetRenderedReportContext): Promise<HttpClientResponse<Report | string | Blob>>

// application/json and text/plain both declared, no binary variant declared at all
async function getNote(context: GetNoteContext): Promise<HttpClientResponse<Note | string | Blob>>

text/event-stream keeps its own precedence, unchanged: an operation offering it becomes a streaming, async-iterable operation exactly as it already does, and never joins a content-type union.

Runtime dispatch

Typing the reply as a union only says what the response might be. Reading it correctly is a runtime decision, made from the response's own Content-Type header — the header states what the server actually sent, which is what the client trusts over the declaration:

  • A JSON Content-Type reads via response.json().
  • A textual Content-Type reads via response.text().
  • Anything else reads via response.blob().

This holds even for a Content-Type the specification never declared at all, in either direction: a server sending image/png for an operation that only declared application/octet-stream still gets read as binary, and a server sending application/json for an operation that only declared text and binary variants still gets parsed as JSON. The actual Content-Type — not the declared one — always decides which reader runs. A response with no Content-Type header at all falls back to the operation's highest-priority declared variant, in the order above (JSON, then text, then binary).

An operation that declares json and text but no binary variant at all still reads a genuinely unrecognized Content-Type as a Blob, never as text: reading an unrecognized body as text would corrupt it if it turns out to be binary bytes, while reading a genuinely textual body as a Blob only inconveniences the caller, who still has the exact bytes intact. Symmetrically, an operation that declares json and binary but no text variant still reads a genuinely textual, undeclared Content-Type as a string, not a Blob — the dispatch classifies by the header's actual shape, not by what the operation happened to declare.

Because of this, both data and rawData are typed to admit all three body shapes — the JSON object (or modeled type), string, and Blob — for any operation with more than one declared variant, whatever subset of them it actually declares; the runtime dispatch can produce any of the three, so the declared type always covers what it can actually assign. A single declared variant is unaffected — its one reader is the only one the dispatch can ever reach, so its reply type stays exactly that one type.

Narrow the union the way you would any other one:

const result = await client.downloadReport({ parameters: { reportId } })
if (result.data instanceof Blob) {
  // an undeclared or declared binary variant
} else if (typeof result.data === 'string') {
  // an undeclared or declared textual variant
} else {
  // the modeled JSON variant
}

rawData holds the same value as data before any cast: the parsed JSON object for the JSON variant, or the same string/Blob value data already holds for the others — there is no separate "raw" form to keep for a body that was never deserialized in the first place. rawData's declared type differs from data's only in the JSON member — Record<string, any> versus the modeled type — since data's JSON member is data cast from rawData.

Why this matters

Before per-variant classification, an operation's response content type was resolved once for the whole operation, and a JSON variant declared anywhere — even on an unrelated status code — made every other declared variant invisible. An operation returning a binary file alongside a JSON error body was generated as pure JSON, so a successful binary response was parsed with response.json() and failed. Declaring a caller-side Accept header to pick one variant was considered and rejected: a server that ignores Accept would still send whatever it sends, and the client would still need to read it by what actually came back.