Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/content/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ export default defineConfig({
items: [
{ text: 'Validation Errors', link: '/docs/advanced/validation-errors' },
{ text: 'RPC Protocol', link: '/docs/advanced/rpc-protocol' },
{ text: 'exceeds the maximum length ...', link: '/docs/advanced/exceeds-the-maximum-length-problem' },
{ text: 'RPC JSON Serializer', link: '/docs/advanced/rpc-json-serializer' },
{ text: 'Exceeds the maximum length ...', link: '/docs/advanced/exceeds-the-maximum-length-problem' },
],
},
{
Expand Down
83 changes: 83 additions & 0 deletions apps/content/docs/advanced/rpc-json-serializer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
title: RPC JSON Serializer
description: Extend or override the standard RPC JSON serializer.
---

# RPC JSON Serializer

This serializer handles JSON payloads for the [RPC Protocol](/docs/advanced/rpc-protocol) and supports [native data types](/docs/rpc-handler#supported-data-types).

## Extending Native Data Types

Extend native types by creating your own `StandardRPCCustomJsonSerializer` and adding it to the `customJsonSerializers` option.

1. **Define Your Custom Serializer**

```ts twoslash
import type { StandardRPCCustomJsonSerializer } from '@orpc/client/standard'

export class User {
constructor(
public readonly id: string,
public readonly name: string,
public readonly email: string,
public readonly age: number,
) {}

toJSON() {
return {
id: this.id,
name: this.name,
email: this.email,
age: this.age,
}
}
}

export const userSerializer: StandardRPCCustomJsonSerializer = {
type: 21,
condition: data => data instanceof User,
serialize: data => data.toJSON(),
deserialize: data => new User(data.id, data.name, data.email, data.age),
}
```

::: warning
Ensure the `type` is unique and greater than `20` to avoid conflicts with [built-in types](/docs/advanced/rpc-protocol#supported-types) in the future.
:::

2. **Use Your Custom Serializer**

```ts twoslash
import type { StandardRPCCustomJsonSerializer } from '@orpc/client/standard'
import { RPCHandler } from '@orpc/server/fetch'
import { RPCLink } from '@orpc/client/fetch'
declare const router: Record<never, never>
declare const userSerializer: StandardRPCCustomJsonSerializer
// ---cut---
const handler = new RPCHandler(router, {
customJsonSerializers: [userSerializer], // [!code highlight]
})

const link = new RPCLink({
url: 'https://example.com/rpc',
customJsonSerializers: [userSerializer], // [!code highlight]
})
```

## Overriding Built-in Types

You can override built-in types by matching their `type` with the [built-in types](/docs/advanced/rpc-protocol#supported-types).

For example, oRPC represents `undefined` only in array items and ignores it in objects. To override this behavior:

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

export const undefinedSerializer: StandardRPCCustomJsonSerializer = {
type: 3, // Match the built-in undefined type. [!code highlight]
condition: data => data === undefined,
serialize: data => null, // JSON cannot represent undefined, so use null.
deserialize: data => undefined,
}
```
4 changes: 2 additions & 2 deletions apps/content/docs/best-practices/dedupe-middleware.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
Title: Dedupe Middleware
Description: Enhance oRPC middleware performance by avoiding redundant executions.
title: Dedupe Middleware
description: Enhance oRPC middleware performance by avoiding redundant executions.
---

# Dedupe Middleware
Expand Down
4 changes: 2 additions & 2 deletions apps/content/docs/lifecycle.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
Title: Lifecycle
Description: Master the oRPC lifecycle to confidently implement and customize your procedures.
title: Lifecycle
description: Master the oRPC lifecycle to confidently implement and customize your procedures.
---

# Lifecycle
Expand Down
4 changes: 2 additions & 2 deletions apps/content/docs/metadata.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
Title: Metadata
Description: Enhance your procedures with metadata.
title: Metadata
description: Enhance your procedures with metadata.
---

# Metadata
Expand Down
143 changes: 143 additions & 0 deletions packages/client/src/adapters/standard/rpc-json-serializer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { supportedDataTypes } from '../../../tests/shared'
import { StandardRPCJsonSerializer } from './rpc-json-serializer'

class Person {
constructor(
public name: string,
public date: Date,
) {}

toJSON() {
return {
name: this.name,
date: this.date,
}
}
}

class Person2 {
constructor(
public name: string,
public data: any,
) { }

toJSON() {
return {
name: this.name,
data: this.data,
}
}
}

const customSupportedDataTypes: { name: string, value: unknown, expected: unknown }[] = [
{
name: 'person - 1',
value: new Person('unnoq', new Date('2023-01-01')),
expected: new Person('unnoq', new Date('2023-01-01')),
},
{
name: 'person - 2',
value: new Person2('unnoq - 2', [{ nested: new Date('2023-01-02') }, /uic/gi]),
expected: new Person2('unnoq - 2', [{ nested: new Date('2023-01-02') }, /uic/gi]),
},
]

describe.each([
...supportedDataTypes,
...customSupportedDataTypes,
])('standardRPCJsonSerializer: $name', ({ value, expected }) => {
const serializer = new StandardRPCJsonSerializer({
customJsonSerializers: [
{
type: 20,
condition: data => data instanceof Person,
serialize: data => data.toJSON(),
deserialize: data => new Person(data.name, data.date),
},
{
type: 21,
condition: data => data instanceof Person2,
serialize: data => data.toJSON(),
deserialize: data => new Person2(data.name, data.data),
},
],
})

function assert(value: unknown, expected: unknown) {
const [json, meta, maps, blobs] = serializer.serialize(value)

const deserialized = serializer.deserialize(json, meta, maps, (i: number) => blobs[i]!)
expect(deserialized).toEqual(expected)
}

it('flat', () => {
assert(value, expected)
})

it('nested object', () => {
assert({
data: value,
nested: {
data: value,
},
}, {
data: expected,
nested: {
data: expected,
},
})
})

it('nested array', () => {
assert([value, [value]], [expected, [expected]])
})

it('complex', () => {
assert({
'date': new Date('2023-01-01'),
'regexp': /uic/gi,
'url': new URL('https://unnoq.com'),
'!@#$%^^&()[]>?<~_<:"~+!_': value,
'list': [value],
'map': new Map([[value, value]]),
'set': new Set([value]),
'nested': {
nested: value,
},
}, {
'date': new Date('2023-01-01'),
'regexp': /uic/gi,
'url': new URL('https://unnoq.com'),
'!@#$%^^&()[]>?<~_<:"~+!_': expected,
'list': [expected],
'map': new Map([[expected, expected]]),
'set': new Set([expected]),
'nested': {
nested: expected,
},
})
})
})

describe('standardRPCJsonSerializer: custom serializers', () => {
it('should throw when type is duplicated', () => {
expect(() => {
return new StandardRPCJsonSerializer({
customJsonSerializers: [
{
type: 20,
condition: data => data instanceof Person,
serialize: data => data.toJSON(),
deserialize: data => new Person(data.name, data.date),
},
{
type: 20,
condition: data => data instanceof Person,
serialize: data => data.toJSON(),
deserialize: data => new Person(data.name, data.date),
},
],
})
}).toThrow('Custom serializer type must be unique.')
})
})
Loading