---
title: "RPC Serializer"
description: "Learn how RPC Serializers encode data between client and server, supporting types beyond plain JSON such as Date, BigInt, Set, Map, and custom classes."
---

## Supported Data Types

`RPCSerializer` supports the following types by default:

| Type                             | Handler key | Notes                                |
| -------------------------------- | ----------- | ------------------------------------ |
| **string**                       |             |                                      |
| **number**                       |             |                                      |
| **NaN**                          | `nan`       |                                      |
| **Infinity**, **-Infinity**      | `infinity`  |                                      |
| **boolean**                      |             |                                      |
| **null**                         |             |                                      |
| **undefined**                    | `undefined` | Ignore `undefined` properties        |
| **Date**                         | `date`      | Includes `Invalid Date`.             |
| **BigInt**                       | `bigint`    | [Digit limit](#bigint-digit-limit)   |
| **URL**                          | `url`       |                                      |
| **Record (object)**              |             | `toJSON` methods are ignored         |
| **Array**                        |             |                                      |
| **Set**                          | `set`       |                                      |
| **Map**                          | `map`       |                                      |
| **Blob**                         |             | Unsupported in `AsyncIteratorObject` |
| **File**                         |             | Unsupported in `AsyncIteratorObject` |
| **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](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to read the `Content-Disposition` and `Standard-Server` response headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standard-server#how-body-parsing-works). If you use the [CORS Plugin](/docs/plugins/cors), include them in `exposeHeaders`:

```ts
const cors = new CORSHandlerPlugin({
  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.

```ts twoslash
class Person {
  constructor(
    public name: string,
    public age: number,
  ) {}
}
import * as z from 'zod'
// ---cut---
import { RPCSerializer } from '@orpc/client'

const PersonSchema = z.object({ name: z.string(), age: z.number() })

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) => {
        const { name, age } = PersonSchema.parse(v)
        return new Person(name, age)
      },
    },
    date: { // <- replace the default Date handler
      condition: v => v instanceof Date,
      serialize: (v: Date) => v.getTime(),
      deserialize: v => new Date(z.number().parse(v)),
    },
  },
})
```

:::warning
`deserialize` receives untrusted input. A malicious client controls `meta` and can point any handler at any JSON value, so never assume `deserialize` receives what `serialize` produced. Validate the value and throw when it does not match. Built-in handlers do the same, and [RPC Handler](/docs/rpc/handler) turns these errors into a `BAD_REQUEST` response.
:::

:::info[Use a custom serializer with RPCHandler and RPCLink]

```ts
const handler = new RPCHandler(router, {
  serializer,
})

const link = new RPCLink({
  serializer,
})
```

:::

:::info[Handlers out of sync]
Client and server can run different versions, so one side may receive a handler key it doesn't know. The value is then left in its serialized form instead of failing the whole payload, and values nested inside it are still restored. Configure the same handlers on both sides to get the original types back.
:::

## BigInt Digit Limit

The built-in `bigint` handler only accepts decimal strings of up to 4300 digits, since parsing longer ones can block the event loop. To allow longer values, override it on both sides:

```ts
import { RPCSerializer } from '@orpc/client'

const serializer = new RPCSerializer({
  handlers: {
    bigint: {
      condition: v => typeof v === 'bigint',
      serialize: (v: bigint) => v.toString(),
      deserialize: v => BigInt(z.string().max(10_000).regex(/^-?\d+$/).parse(v)),
    },
  },
})
```

## RPC JSON Serializer

`RPCJsonSerializer` is the JSON layer behind `RPCSerializer`. Use it directly wherever only JSON is accepted.

```ts twoslash
import { RPCJsonSerializer } from '@orpc/client'

const serializer = new RPCJsonSerializer({
  handlers: {
    // same handlers as RPCSerializer
  },
})

const { json, meta } = serializer.serializeJson({
  name: 'John',
  createdAt: new Date('2024-01-01'),
})
// json: { name: 'John', createdAt: '2024-01-01T00:00:00.000Z' }
// meta: [['date', 'createdAt']]

const data = serializer.deserializeJson({ json, meta })
// data: { name: 'John', createdAt: Date }
```

:::info[Reuse an RPC Serializer]
`RPCSerializer` extends `RPCJsonSerializer`, so an `RPCSerializer` instance also works wherever an `RPCJsonSerializer` is accepted, such as [Publisher adapters](/docs/helpers/publisher#adapters), [TanStack Query keys](/docs/integrations/tanstack-query#key-serializer) and [hydration](/docs/integrations/tanstack-query#custom-serializers), or [Pinia Colada keys](/docs/integrations/pinia-colada#key-serializer). Configure your handlers once and share the same instance.
:::

## 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](#supported-data-types)).
- **path**: Path to the value inside `json`.

:::

```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')`.
:::

```ts
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
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](https://github.com/middleapi/standard-server#how-body-parsing-works).
:::

### AsyncIteratorObject

When the output is an `AsyncIteratorObject`, it is sent as a [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) stream. Each event contains one serialized chunk of data.

```http
HTTP/1.1 200 OK
Content-Type: text/event-stream

data: {"json":{"name":"John","createdAt":"2024-01-01T00:00:00.000Z"},"meta":[["date","createdAt"]]}

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
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](https://github.com/middleapi/standard-server#how-body-parsing-works).
:::

## Learn More

The serializers are small, self-contained modules, making them easy to understand.
To explore their behavior in detail, see the source code of [`RPCSerializer`](https://github.com/middleapi/orpc/blob/main/packages/client/src/rpc-serializer.ts) and [`RPCJsonSerializer`](https://github.com/middleapi/orpc/blob/main/packages/client/src/rpc-json-serializer.ts).
