Multi-Content-Type Example
Build a Report Export API where reports can be retrieved as JSON or downloaded as CSV/PDF. This example demonstrates using multiple content types on a single response, a common pattern for content negotiation.
Define Your Schemas
First, define schemas using any Standard Schema-compatible library. The examples below use placeholder variables -- replace them with your Zod, ArkType, Valibot, or other Standard Schema objects.
// Your Zod/ArkType/Valibot schemas -- any Standard Schema-compatible library works
const ReportSchema = /* your schema here */;
const CreateReportSchema = /* your schema here */; // e.g. { title, startDate, endDate }
const ErrorSchema = /* your schema here */;Generate the OpenAPI Spec
Use the openapi() function to declare routes with multi-content-type responses. The key is using a full ResponseObject for status codes that need multiple content types.
import { openapi } from "to-openapi";
const spec = openapi({
info: {
title: "Report Export API",
version: "1.0.0",
description: "An API for generating and exporting reports",
},
servers: [
{ url: "https://api.example.com/v1", description: "Production" },
],
tags: [
{ name: "Reports", description: "Report generation and export" },
],
schemas: {
Report: ReportSchema,
Error: ErrorSchema,
},
paths: {
// Create a new report
"POST /reports": {
summary: "Create a report",
description: "Generate a new report from the provided parameters.",
tags: ["Reports"],
operationId: "createReport",
body: CreateReportSchema,
201: ReportSchema,
400: ErrorSchema,
},
// Get a report as JSON or CSV
"GET /reports/{id}": {
summary: "Get a report",
description: "Retrieve a report as JSON or download as CSV.",
tags: ["Reports"],
operationId: "getReport",
200: {
description: "The requested report",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Report" },
},
"text/csv": {
schema: { type: "string" },
},
},
},
404: ErrorSchema,
},
// Export a report as JSON or PDF
"GET /reports/{id}/export": {
summary: "Export a report",
description: "Export a report as JSON data or a PDF document.",
tags: ["Reports"],
operationId: "exportReport",
200: {
description: "The exported report",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Report" },
},
"application/pdf": {
schema: { type: "string", format: "binary" },
},
},
},
404: ErrorSchema,
},
},
});Key details:
- ResponseObject passthrough — when a status code value has a
descriptionorcontentproperty (and no~standardproperty), it is passed through to the OpenAPI output as-is. - Multiple content types — list each media type under
contentwith its own schema. Use{ type: "string" }for text formats like CSV, and{ type: "string", format: "binary" }for binary formats like PDF. $refusage — reference named schemas registered via theschemasoption. To appear incomponents/schemas, a schema must also be used through the shorthand system on at least one route (e.g.,201: ReportSchemaonPOST /reports).
Generated Output
The resulting spec is a fully valid OpenAPI 3.1.0 document. Here is an abbreviated look at the multi-content-type response structure:
{
"openapi": "3.1.0",
"info": {
"title": "Report Export API",
"version": "1.0.0",
"description": "An API for generating and exporting reports"
},
"paths": {
"/reports": {
"post": {
"operationId": "createReport",
"summary": "Create a report",
"tags": ["Reports"],
"requestBody": {
"content": {
"application/json": { "schema": { "..." : "..." } }
}
},
"responses": {
"201": {
"description": "Resource created",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/Report" }
}
}
}
}
}
},
"/reports/{id}": {
"get": {
"operationId": "getReport",
"summary": "Get a report",
"tags": ["Reports"],
"parameters": [
{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
],
"responses": {
"200": {
"description": "The requested report",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/Report" }
},
"text/csv": {
"schema": { "type": "string" }
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/Error" }
}
}
}
}
}
},
"/reports/{id}/export": {
"get": {
"operationId": "exportReport",
"summary": "Export a report",
"tags": ["Reports"],
"parameters": [
{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
],
"responses": {
"200": {
"description": "The exported report",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/Report" }
},
"application/pdf": {
"schema": { "type": "string", "format": "binary" }
}
}
}
}
}
}
},
"components": {
"schemas": {
"Report": { "..." : "..." },
"Error": { "..." : "..." }
}
}
}Using the Class API
The same spec can be built incrementally with the OpenAPI class.
import { OpenAPI } from "to-openapi";
const api = new OpenAPI({
info: {
title: "Report Export API",
version: "1.0.0",
description: "An API for generating and exporting reports",
},
servers: [
{ url: "https://api.example.com/v1", description: "Production" },
],
tags: [
{ name: "Reports", description: "Report generation and export" },
],
});
api.schema("Report", ReportSchema);
api.schema("Error", ErrorSchema);
api.route("post", "/reports", {
summary: "Create a report",
tags: ["Reports"],
operationId: "createReport",
body: CreateReportSchema,
201: ReportSchema,
400: ErrorSchema,
});
api.route("get", "/reports/{id}", {
summary: "Get a report",
tags: ["Reports"],
operationId: "getReport",
200: {
description: "The requested report",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Report" },
},
"text/csv": {
schema: { type: "string" },
},
},
},
404: ErrorSchema,
});
api.route("get", "/reports/{id}/export", {
summary: "Export a report",
tags: ["Reports"],
operationId: "exportReport",
200: {
description: "The exported report",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Report" },
},
"application/pdf": {
schema: { type: "string", format: "binary" },
},
},
},
404: ErrorSchema,
});
const spec = api.document();Both approaches produce identical output. Use whichever style fits your project.