Skip to content

Commit e834b92

Browse files
committed
fix: invoke toJSON methods if present
1 parent 95530a6 commit e834b92

File tree

4 files changed

+85
-6
lines changed

4 files changed

+85
-6
lines changed

spec.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,24 @@ A property name of `__proto__` is forbidden.
6666

6767
An empty object is encoded as `{}`.
6868

69+
#### Object coercion
70+
71+
If an object has a `toJSON` method, it will be invoked and the result will be encoded instead of the object itself. The `toJSON` method may have properties whose values also have `toJSON` methods, and those will be invoked recursively.
72+
73+
```ts
74+
{
75+
a: { ignoredKey: true, toJSON: () => ({ b: 1 }) }
76+
}
77+
```
78+
79+
…is encoded into the following query string:
80+
81+
```
82+
a={b:1}
83+
```
84+
85+
Note: If the root object has a `toJSON` method, it needs to return an object.
86+
6987
## Strings
7088

7189
Strings are not wrapped in quotes. If a string would lead to ambiguity, its characters may be escaped with a backslash (`\`) as required.

src/encode.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,37 @@
11
import { isArray } from 'radashi'
22
import { CharCode, isDigit } from './charCode.js'
3-
import { CodableObject, CodableValue } from './types.js'
3+
import { CodableObject, CodableRecord, CodableValue } from './types.js'
44

55
export type EncodeOptions = {
66
skippedKeys?: string[]
77
}
88

99
export function encode(obj: CodableObject, options?: EncodeOptions): string {
10-
return encodeProperties(obj, false, options?.skippedKeys)
10+
return encodeProperties(
11+
isRecord(obj) ? obj : assertRecord(obj.toJSON()),
12+
false,
13+
options?.skippedKeys
14+
)
15+
}
16+
17+
function assertRecord(value: CodableValue): CodableRecord {
18+
if (
19+
typeof value !== 'object' ||
20+
isArray(value) ||
21+
value === null ||
22+
!isRecord(value)
23+
) {
24+
throw new Error('Expected toJSON method to return an object')
25+
}
26+
return value
27+
}
28+
29+
function isRecord(value: CodableObject): value is CodableRecord {
30+
return typeof value.toJSON !== 'function'
1131
}
1232

1333
function encodeProperties(
14-
obj: CodableObject,
34+
obj: CodableRecord,
1535
nested: boolean,
1636
skippedKeys?: string[]
1737
): string {
@@ -78,7 +98,10 @@ function encodeValue(value: CodableValue): string {
7898
return encodeArray(value)
7999
}
80100
if (typeof value === 'object') {
81-
return encodeObject(value)
101+
if (isRecord(value)) {
102+
return encodeObject(value)
103+
}
104+
return encodeValue(value.toJSON())
82105
}
83106
if (typeof value === 'bigint') {
84107
return String(value) + 'n'
@@ -110,7 +133,7 @@ function encodeArray(array: readonly CodableValue[]): string {
110133
return `(${result})`
111134
}
112135

113-
function encodeObject(obj: CodableObject): string {
136+
function encodeObject(obj: CodableRecord): string {
114137
return `{${encodeProperties(obj, true)}}`
115138
}
116139

src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
export type CodableObject = { [key: string]: CodableValue }
1+
export type CodableObject = { toJSON(): CodableValue } | CodableRecord
2+
3+
export type CodableRecord = { [key: string]: CodableValue }
24

35
export type CodableValue =
46
| string

test/encode.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,42 @@ describe('json-qs', () => {
1212
})
1313
}
1414

15+
test('object with toJSON method', () => {
16+
// Root object
17+
expect(
18+
encode({
19+
ignoredKey: true,
20+
toJSON: () => ({ a: 1 }),
21+
})
22+
).toBe('a=1')
23+
24+
// Nested object
25+
expect(
26+
encode({
27+
a: {
28+
b: { toJSON: () => 1 },
29+
},
30+
})
31+
).toBe('a={b:1}')
32+
33+
// Recursive toJSON calls
34+
expect(
35+
encode({
36+
a: {
37+
b: {
38+
ignoredKey: true,
39+
toJSON: () => ({
40+
c: {
41+
ignoredKey: true,
42+
toJSON: () => 1,
43+
},
44+
}),
45+
},
46+
},
47+
})
48+
).toBe('a={b:{c:1}}')
49+
})
50+
1551
test('negative zero (not preserved)', () => {
1652
expect(encode({ a: -0 })).toBe('a=0')
1753
})

0 commit comments

Comments
 (0)