OpenAPI Input and Output Mapping
oRPC lets you map OpenAPI requests and responses to procedure inputs and outputs in a few different ways.
Input Mapping
By default, oRPC uses compact mode where path parameters are merged with either query parameters or the request body, depending on the HTTP method.
const searchPlanets = os
.meta(openapi({ method: 'GET', path: '/planets/{id}' }))
.input(z.object({
id: z.string(),
q: z.string().optional(),
}))
.handler(async ({ input }) => {
return { id: input.id, q: input.q }
})
For GET /planets/earth?q=life, the procedure receives:
{
"id": "earth",
"q": "life"
}
Detailed Input Structure
In detailed mode, the input is an object with separate params, query, headers, and body fields.
const updatePlanet = os
.meta(openapi({
method: 'POST',
path: '/planets/{id}',
inputStructure: 'detailed',
}))
.input(z.object({
params: z.object({ id: z.string() }),
query: z.object({ dryRun: z.coerce.boolean().optional() }).optional(),
headers: z.object({ 'x-trace-id': z.string() }).optional(),
body: z.object({ name: z.string() }),
}))
.handler(async ({ input }) => {
return input
})
For POST /planets/earth?dryRun=true with header x-trace-id: abc123 and body { "name": "Earth" }, the procedure receives:
{
"params": { "id": "earth" },
"query": { "dryRun": true },
"headers": { "x-trace-id": "abc123" },
"body": { "name": "Earth" }
}
Path Parameter Styles
By default, path parameters are decoded as plain strings. Use paramsStyles to override how each path parameter is encoded and decoded.
const getPlanets = os
.meta(openapi({
method: 'GET',
path: '/planets/{ids}/{filters}',
paramsStyles: {
ids: 'comma-delimited-array',
filters: 'comma-delimited-object',
},
}))
.input(z.object({
ids: z.array(z.string()),
filters: z.object({
type: z.string(),
status: z.string(),
}),
}))
.handler(async () => [])
Supported path parameter styles:
| Style | Example path segment | Decoded value |
|---|---|---|
primitive (default) |
/planets/earth |
{ id: 'earth' } |
comma-delimited-array |
/planets/earth,mars |
{ ids: ['earth', 'mars'] } |
comma-delimited-object |
/planets/type,rocky,status,known |
{ filters: { type: 'rocky', status: 'known' } } |
Query Styles
By default, query parameters are decoded with bracket notation. Use queryStyles to override how each query parameter is encoded and decoded.
const searchPlanets = os
.meta(openapi({
method: 'GET',
path: '/planets',
queryStyles: {
keyword: 'primitive',
tags: 'comma-delimited-array',
filters: 'comma-delimited-object',
meta: 'json',
},
}))
.handler(async () => [])
Supported query styles:
| Style | Example | Decoded value |
|---|---|---|
primitive |
?tag=a&tag=b |
{ tag: 'b' } |
array |
?tag=a&tag=b |
{ tag: ['a', 'b'] } |
comma-delimited-array |
?tags=red,blue |
{ tags: ['red', 'blue'] } |
comma-delimited-object |
?filter=size,large,brand,nike |
{ filter: { size: 'large', brand: 'nike' } } |
space-delimited-array |
?tags=red blue |
{ tags: ['red', 'blue'] } |
space-delimited-object |
?filter=size large brand nike |
{ filter: { size: 'large', brand: 'nike' } } |
pipe-delimited-array |
?tags=red|blue |
{ tags: ['red', 'blue'] } |
pipe-delimited-object |
?filter=size|large|brand|nike |
{ filter: { size: 'large', brand: 'nike' } } |
json |
?meta={"enabled":true} |
{ meta: { enabled: true } } |
| default | ?tags[]=red&tags[]=blue&filter[status]=active |
{ tags: ['red', 'blue'], filter: { status: 'active' } } |
Output Mapping
By default, oRPC uses compact mode. The procedure’s return value becomes the response body, and the status code comes from successStatus, which defaults to 200 (should be in the 2xx range and must be less than 400).
const getPlanet = os
.meta(openapi({ method: 'GET', path: '/planets', successStatus: 200 }))
.handler(async () => {
return { id: 'earth', name: 'Earth' }
})
Detailed Output Structure
In detailed mode, return an object with the following fields:
status: optional success status code (defaults tosuccessStatus, should be in the2xxrange and must be less than400)headers: optional response headers in lower-case keysbody: optional response body
const savePlanet = os
.meta(openapi({
method: 'PUT',
path: '/planets/{id}',
outputStructure: 'detailed',
successStatus: 200,
}))
.input(z.object({ id: z.string() }))
.output(z.union([
z.object({
status: z.literal(201).meta({ description: 'Created' }),
body: z.object({ id: z.string(), name: z.string() }),
}),
z.object({
status: z.literal(200).meta({ description: 'Updated' }),
body: z.object({ id: z.string(), name: z.string() }),
}),
]))
.handler(async ({ input }) => {
if (!isExistingPlanet(input.id)) {
return {
status: 201,
headers: { 'x-created': 'true' },
body: { id: 'earth', name: 'Earth' },
}
}
return {
body: { id: 'earth', name: 'Earth' },
}
})
Body Hints
The body parser normally uses Content-Type, Content-Length, Content-Disposition, and Standard-Server headers to decide how to parse the body. If that information is missing or misleading, use requestBodyHint to tell OpenAPI Handler how to parse the request body. Likewise, use responseBodyHint to tell OpenAPI Link how to parse the response body.
const uploadLargeFile = os
.meta(openapi({
requestBodyHint: 'octet-stream',
responseBodyHint: 'json',
}))
.input(z.instanceof(ReadableStream))
.handler(async ({ input }) => {
for await (const chunk of input) {
// process chunk
}
return { ok: true }
})
Supported body hints:
| Hint | Parsed Result |
|---|---|
json |
JSON value |
form-data |
FormData decoded with bracket notation |
url-search-params |
URLSearchParams decoded with bracket notation |
event-stream |
AsyncIteratorObject |
octet-stream |
ReadableStream<Uint8Array> for streamed binary data |
file |
File for binary data |
none |
undefined |
Metadata Merging
When openapi is applied multiple times, paramsStyles and queryStyles are merged per parameter, and the most recent style defined for a parameter wins. inputStructure, outputStructure, responseBodyHint, and requestBodyHint are overridden by the most recent call. For the full merge behavior of every field, see Metadata Merging.
const router = os
.meta(openapi({ inputStructure: 'detailed' }))
.router({
get: os
.meta(openapi({ method: 'GET', path: '/planets', inputStructure: 'compact' }))
.meta(openapi({ queryStyles: { tags: 'comma-delimited-array' } }))
.meta(openapi({ queryStyles: { q: 'primitive' } }))
.input(z.object({ tags: z.array(z.string()), q: z.string().optional() }))
.handler(async () => ([])),
})
These are equivalent to:
const router = {
get: os
.meta(openapi({
method: 'GET',
path: '/planets',
inputStructure: 'compact',
queryStyles: {
tags: 'comma-delimited-array',
q: 'primitive',
},
}))
.input(z.object({ tags: z.array(z.string()), q: z.string().optional() }))
.handler(async () => ([])),
}