RPC Serializer
RPC Serializers handle the serialization and deserialization of data sent between the client and server. They allow you to support complex data types beyond plain JSON, such as Date, BigInt, Set, and even custom classes.
Supported Data Types
RPCSerializer supports the following types by default:
| Type | Handler key | Notes |
|---|---|---|
| string | ||
| number | ||
| NaN | nan | |
| boolean | ||
| null | ||
| undefined | undefined | Ignore undefined properties |
| Date | date | Includes Invalid Date. |
| BigInt | bigint | |
| RegExp | regexp | |
| URL | url | |
| Record (object) | toJSON methods are ignored | |
| Array | ||
| Set | set | |
| Map | map | |
| Blob | Unsupported in Event Iterator | |
| File | None | Unsupported in Event Iterator |
| Event Iterator (AsyncIteratorObject) | Only at the root level | |
| ReadableStream<Uint8Array> | Only at the root level |
WARNING
To better support Blob, File, and ReadableStream<Uint8Array> at the root level in cross-origin scenarios, extend your CORS allowlist to allow clients to send and receive the Content-Disposition and Standard-Server headers. Learn more in the Standard Server documentation. If you use the CORS Plugin, include them in allowHeaders and exposeHeaders:
const cors = new CORSHandlerPlugin({
allowHeaders: ['Content-Disposition', 'Standard-Server'],
exposeHeaders: ['Content-Disposition', 'Standard-Server'],
})Custom Serializers
Add custom handlers with unique keys to support additional types, or reuse a built-in key to override the default behavior.
import { RPCSerializer } from '@orpc/client'
const serializer = new RPCSerializer({
handlers: {
person: { // <- add support for Person
condition: v => v instanceof Person,
serialize: (v: Person) => ({ name: v.name, age: v.age }),
deserialize: v => new Person(v.name, v.age),
},
date: { // <- replace the default Date handler
condition: v => v instanceof Date,
serialize: (v: Date) => v.getTime(),
deserialize: v => new Date(v),
},
},
})Use a custom serializer with RPCHandler and RPCLink
const handler = new RPCHandler(router, {
serializer,
})
const link = new RPCLink({
serializer,
})Serialization Format
In most cases, serialized data includes two optional fields: json and meta. json contains JSON-serializable data. meta contains the metadata needed to deserialize values.
INFO
meta is stored in the format [handler: string, ...path: (string | number)[]].
- handler: The handler key used for serialization (see Supported Data Types).
- path: Path to the value inside
json.
{
"json": {
"name": "John",
"age": 30,
"createdAt": "2024-01-01T00:00:00.000Z"
},
"meta": [
["date", "createdAt"]
]
}With Files
If the data includes Blob or File, the serializer returns a FormData object. The data field contains a JSON string with json, meta, and maps, and the remaining fields contain the file parts.
INFO
maps is stored in the format [...path: (string | number)[]], and its order corresponds to the file parts in the FormData.
For example, [['thumbnail'], ['images', 0]] means the first file part corresponds to json.thumbnail at form.get('0'), and the second file part corresponds to json.images[0] at form.get('1').
const form = new FormData()
form.set('data', JSON.stringify({
json: {
name: 'Earth',
thumbnail: {},
images: [{}],
createdAt: '2022-01-01T00:00:00.000Z'
},
meta: [['date', 'createdAt']],
maps: [['thumbnail'], ['images', 0]]
}))
form.set('0', new Blob([''], { type: 'image/png' }))
form.set('1', new Blob([''], { type: 'image/png' }))Direct File
If the entire data is a single Blob or File, it can be sent as-is without wrapping in FormData.
HTTP/1.1 200 OK
Content-Type: image/png
Content-Disposition: attachment; filename="earth.png"
Content-Length: 12345
Standard-Server: file
<binary data>INFO
If the receiver mistakenly handles this payload as a regular (non-file) body, set the standard-server header to help the receiver detect the actual data type and handle it correctly. Learn more about this header in the Standard Server Documentation.
Event Iterator (AsyncIteratorObject)
When the output is an event iterator (AsyncIteratorObject), it is sent as a Server-Sent Events stream. Each event contains one serialized chunk of data.
HTTP/1.1 200 OK
Content-Type: text/event-stream
event: message
data: {"json":{"name":"John","createdAt":"2024-01-01T00:00:00.000Z"},"meta":[["date","createdAt"]]}
event: message
data: {"json":{"name":"Jane","createdAt":"2024-01-02T00:00:00.000Z"},"meta":[["date","createdAt"]]}ReadableStream<Uint8Array>
A ReadableStream<Uint8Array> is passed through as-is and streamed as binary data.
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Standard-Server: octet-stream
<binary chunk 1>
<binary chunk 2>INFO
If the receiver mistakenly handles this payload as a regular (non-stream) body, set the standard-server header to help the receiver detect the actual data type and handle it correctly. Learn more about this header in the Standard Server Documentation.
Learn More
The serializer is a small, self-contained module, making it easy to understand. To explore its behavior in detail, see the source code.

