Skip to content

Commit 32f3296

Browse files
authored
feat(client, server): custom RPC JSON Serializer (#241)
The way to extend or override built-in types support by RPC Protocol <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Updated the navigation sidebar with a refreshed "Advanced" section that now highlights RPC JSON Serializer and reintroduces the maximum length topic with corrected labeling. - Introduced a comprehensive guide for customizing RPC JSON serialization, offering step-by-step instructions for creating tailored serializers. - **Documentation** - Standardized metadata formatting across documentation pages for improved consistency. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 0f0c680 commit 32f3296

File tree

7 files changed

+302
-46
lines changed

7 files changed

+302
-46
lines changed

apps/content/.vitepress/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ export default defineConfig({
145145
items: [
146146
{ text: 'Validation Errors', link: '/docs/advanced/validation-errors' },
147147
{ text: 'RPC Protocol', link: '/docs/advanced/rpc-protocol' },
148-
{ text: 'exceeds the maximum length ...', link: '/docs/advanced/exceeds-the-maximum-length-problem' },
148+
{ text: 'RPC JSON Serializer', link: '/docs/advanced/rpc-json-serializer' },
149+
{ text: 'Exceeds the maximum length ...', link: '/docs/advanced/exceeds-the-maximum-length-problem' },
149150
],
150151
},
151152
{
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
title: RPC JSON Serializer
3+
description: Extend or override the standard RPC JSON serializer.
4+
---
5+
6+
# RPC JSON Serializer
7+
8+
This serializer handles JSON payloads for the [RPC Protocol](/docs/advanced/rpc-protocol) and supports [native data types](/docs/rpc-handler#supported-data-types).
9+
10+
## Extending Native Data Types
11+
12+
Extend native types by creating your own `StandardRPCCustomJsonSerializer` and adding it to the `customJsonSerializers` option.
13+
14+
1. **Define Your Custom Serializer**
15+
16+
```ts twoslash
17+
import type { StandardRPCCustomJsonSerializer } from '@orpc/client/standard'
18+
19+
export class User {
20+
constructor(
21+
public readonly id: string,
22+
public readonly name: string,
23+
public readonly email: string,
24+
public readonly age: number,
25+
) {}
26+
27+
toJSON() {
28+
return {
29+
id: this.id,
30+
name: this.name,
31+
email: this.email,
32+
age: this.age,
33+
}
34+
}
35+
}
36+
37+
export const userSerializer: StandardRPCCustomJsonSerializer = {
38+
type: 21,
39+
condition: data => data instanceof User,
40+
serialize: data => data.toJSON(),
41+
deserialize: data => new User(data.id, data.name, data.email, data.age),
42+
}
43+
```
44+
45+
::: warning
46+
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.
47+
:::
48+
49+
2. **Use Your Custom Serializer**
50+
51+
```ts twoslash
52+
import type { StandardRPCCustomJsonSerializer } from '@orpc/client/standard'
53+
import { RPCHandler } from '@orpc/server/fetch'
54+
import { RPCLink } from '@orpc/client/fetch'
55+
declare const router: Record<never, never>
56+
declare const userSerializer: StandardRPCCustomJsonSerializer
57+
// ---cut---
58+
const handler = new RPCHandler(router, {
59+
customJsonSerializers: [userSerializer], // [!code highlight]
60+
})
61+
62+
const link = new RPCLink({
63+
url: 'https://example.com/rpc',
64+
customJsonSerializers: [userSerializer], // [!code highlight]
65+
})
66+
```
67+
68+
## Overriding Built-in Types
69+
70+
You can override built-in types by matching their `type` with the [built-in types](/docs/advanced/rpc-protocol#supported-types).
71+
72+
For example, oRPC represents `undefined` only in array items and ignores it in objects. To override this behavior:
73+
74+
```ts twoslash
75+
import { StandardRPCCustomJsonSerializer } from '@orpc/client/standard'
76+
77+
export const undefinedSerializer: StandardRPCCustomJsonSerializer = {
78+
type: 3, // Match the built-in undefined type. [!code highlight]
79+
condition: data => data === undefined,
80+
serialize: data => null, // JSON cannot represent undefined, so use null.
81+
deserialize: data => undefined,
82+
}
83+
```

apps/content/docs/best-practices/dedupe-middleware.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
2-
Title: Dedupe Middleware
3-
Description: Enhance oRPC middleware performance by avoiding redundant executions.
2+
title: Dedupe Middleware
3+
description: Enhance oRPC middleware performance by avoiding redundant executions.
44
---
55

66
# Dedupe Middleware

apps/content/docs/lifecycle.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
2-
Title: Lifecycle
3-
Description: Master the oRPC lifecycle to confidently implement and customize your procedures.
2+
title: Lifecycle
3+
description: Master the oRPC lifecycle to confidently implement and customize your procedures.
44
---
55

66
# Lifecycle

apps/content/docs/metadata.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
2-
Title: Metadata
3-
Description: Enhance your procedures with metadata.
2+
title: Metadata
3+
description: Enhance your procedures with metadata.
44
---
55

66
# Metadata
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { supportedDataTypes } from '../../../tests/shared'
2+
import { StandardRPCJsonSerializer } from './rpc-json-serializer'
3+
4+
class Person {
5+
constructor(
6+
public name: string,
7+
public date: Date,
8+
) {}
9+
10+
toJSON() {
11+
return {
12+
name: this.name,
13+
date: this.date,
14+
}
15+
}
16+
}
17+
18+
class Person2 {
19+
constructor(
20+
public name: string,
21+
public data: any,
22+
) { }
23+
24+
toJSON() {
25+
return {
26+
name: this.name,
27+
data: this.data,
28+
}
29+
}
30+
}
31+
32+
const customSupportedDataTypes: { name: string, value: unknown, expected: unknown }[] = [
33+
{
34+
name: 'person - 1',
35+
value: new Person('unnoq', new Date('2023-01-01')),
36+
expected: new Person('unnoq', new Date('2023-01-01')),
37+
},
38+
{
39+
name: 'person - 2',
40+
value: new Person2('unnoq - 2', [{ nested: new Date('2023-01-02') }, /uic/gi]),
41+
expected: new Person2('unnoq - 2', [{ nested: new Date('2023-01-02') }, /uic/gi]),
42+
},
43+
]
44+
45+
describe.each([
46+
...supportedDataTypes,
47+
...customSupportedDataTypes,
48+
])('standardRPCJsonSerializer: $name', ({ value, expected }) => {
49+
const serializer = new StandardRPCJsonSerializer({
50+
customJsonSerializers: [
51+
{
52+
type: 20,
53+
condition: data => data instanceof Person,
54+
serialize: data => data.toJSON(),
55+
deserialize: data => new Person(data.name, data.date),
56+
},
57+
{
58+
type: 21,
59+
condition: data => data instanceof Person2,
60+
serialize: data => data.toJSON(),
61+
deserialize: data => new Person2(data.name, data.data),
62+
},
63+
],
64+
})
65+
66+
function assert(value: unknown, expected: unknown) {
67+
const [json, meta, maps, blobs] = serializer.serialize(value)
68+
69+
const deserialized = serializer.deserialize(json, meta, maps, (i: number) => blobs[i]!)
70+
expect(deserialized).toEqual(expected)
71+
}
72+
73+
it('flat', () => {
74+
assert(value, expected)
75+
})
76+
77+
it('nested object', () => {
78+
assert({
79+
data: value,
80+
nested: {
81+
data: value,
82+
},
83+
}, {
84+
data: expected,
85+
nested: {
86+
data: expected,
87+
},
88+
})
89+
})
90+
91+
it('nested array', () => {
92+
assert([value, [value]], [expected, [expected]])
93+
})
94+
95+
it('complex', () => {
96+
assert({
97+
'date': new Date('2023-01-01'),
98+
'regexp': /uic/gi,
99+
'url': new URL('https://unnoq.com'),
100+
'!@#$%^^&()[]>?<~_<:"~+!_': value,
101+
'list': [value],
102+
'map': new Map([[value, value]]),
103+
'set': new Set([value]),
104+
'nested': {
105+
nested: value,
106+
},
107+
}, {
108+
'date': new Date('2023-01-01'),
109+
'regexp': /uic/gi,
110+
'url': new URL('https://unnoq.com'),
111+
'!@#$%^^&()[]>?<~_<:"~+!_': expected,
112+
'list': [expected],
113+
'map': new Map([[expected, expected]]),
114+
'set': new Set([expected]),
115+
'nested': {
116+
nested: expected,
117+
},
118+
})
119+
})
120+
})
121+
122+
describe('standardRPCJsonSerializer: custom serializers', () => {
123+
it('should throw when type is duplicated', () => {
124+
expect(() => {
125+
return new StandardRPCJsonSerializer({
126+
customJsonSerializers: [
127+
{
128+
type: 20,
129+
condition: data => data instanceof Person,
130+
serialize: data => data.toJSON(),
131+
deserialize: data => new Person(data.name, data.date),
132+
},
133+
{
134+
type: 20,
135+
condition: data => data instanceof Person,
136+
serialize: data => data.toJSON(),
137+
deserialize: data => new Person(data.name, data.date),
138+
},
139+
],
140+
})
141+
}).toThrow('Custom serializer type must be unique.')
142+
})
143+
})

0 commit comments

Comments
 (0)