Skip to content

Commit 9d6cde7

Browse files
committed
feat: 🎸 Add UNSAFE methods
1 parent 20dad1d commit 9d6cde7

6 files changed

Lines changed: 42 additions & 13 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,19 @@ const restored = jsonWeb3.parse(text)
4949
headers: Map(1) {...},
5050
re: /([^\s]+)/g,
5151
url: URL(...),
52-
fn: { "__@json.function__": "function echo(arg) { return arg }" }
52+
fn: undefined
5353
}
5454
*/
5555

56-
const restoredUnsafe = jsonWeb3.parse_UNSAFE(text)
56+
const textUnsafe = jsonWeb3.stringify_UNSAFE(payload)
57+
const restoredUnsafe = jsonWeb3.parse_UNSAFE(textUnsafe)
5758
// restoredUnsafe.fn is a callable function
5859
```
5960

6061
## API (Fully compatible with native globalThis.JSON)
6162

6263
- `stringify(value, replacer?, space?)`
64+
- `stringify_UNSAFE(value, replacer?, space?)` (serializes `Function` payloads)
6365
- `parse(text, reviver?)`
6466
- `parse_UNSAFE(text, reviver?)` (revives `Function` payloads via `new Function(...)`)
6567

@@ -71,7 +73,7 @@ const restoredUnsafe = jsonWeb3.parse_UNSAFE(text)
7173
- `Map` values are encoded as `{"__@json.map__":[[k,v],...]}` and `Set` values as `{"__@json.set__":[...]}`.
7274
- `RegExp` values are encoded as `{"__@json.regexp__":{"source":"...","flags":"..."}}`.
7375
- `URL` values are encoded as `{"__@json.url__":"..."}`.
74-
- `Function` values are encoded as `{"__@json.function__":"<source>"}` and are only revived by `parse_UNSAFE` using `new Function(...)` (only do this with trusted input).
76+
- `Function` values are encoded as `{"__@json.function__":"<source>"}` by `stringify_UNSAFE` and are only revived by `parse_UNSAFE` using `new Function(...)` (only do this with trusted input).
7577
- `ArrayBuffer`, Node `Buffer` JSON shapes, and typed arrays are encoded as `{"__@json.typedarray__":{"type":"<Name>","bytes":"0x..."}}` and decoded back to the original typed array (`Uint8Array`, `Uint8ClampedArray`, `Uint16Array`, `Uint32Array`, `Int8Array`, `Int16Array`, `Int32Array`, `Float32Array`, `Float64Array`, `BigInt64Array`, `BigUint64Array`).
7678

7779
Compared to libraries that require eval-based parsing (for example, `serialize-javascript`), this approach is generally safer and more efficient.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-web3",
3-
"version": "1.1.0",
3+
"version": "1.2.0",
44
"description": "BigInt-safe JSON serialization and deserialization for Web3 use cases.",
55
"keywords": [
66
"json",

src/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,22 @@ export const stringify = (value: any, replacer: Replacer = null, space?: string
5858
space,
5959
)
6060

61+
export const stringify_UNSAFE = (
62+
value: any,
63+
replacer: Replacer = null,
64+
space?: string | number,
65+
): string =>
66+
RAW_JSON.stringify(
67+
value,
68+
function replacerFn(key, v) {
69+
const holderValue = this && key in Object(this) ? (this as any)[key] : v
70+
const candidate = isDate(holderValue) || isURL(holderValue) ? holderValue : v
71+
const replaced = applyReplacer(this, key, candidate, replacer)
72+
return toSerializable(replaced, { allowFunction: true })
73+
},
74+
space,
75+
)
76+
6177
export const parse = <T = any>(text: string, reviver: Reviver = null): T =>
6278
RAW_JSON.parse(text, (key, v) => {
6379
const decoded = fromSerializable(v)
@@ -72,6 +88,7 @@ export const parse_UNSAFE = <T = any>(text: string, reviver: Reviver = null): T
7288

7389
export default {
7490
stringify,
91+
stringify_UNSAFE,
7592
parse,
7693
parse_UNSAFE,
7794
}

src/utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export const fromHex = (hex: string): Uint8Array => {
9090
return out
9191
}
9292

93-
export const toSerializable = (value: any): any => {
93+
export const toSerializable = (value: any, options: { allowFunction?: boolean } = {}): any => {
9494
if (isBigInt(value)) {
9595
return { [BIGINT_TAG]: value.toString() }
9696
}
@@ -120,6 +120,9 @@ export const toSerializable = (value: any): any => {
120120
}
121121

122122
if (isFunction(value)) {
123+
if (!options.allowFunction) {
124+
return value
125+
}
123126
return { [FUNCTION_TAG]: value.toString() }
124127
}
125128

test/index.test.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Script, createContext } from 'node:vm'
22
import { describe, expect, it } from 'vitest'
3-
import jsonWeb3, { parse, parse_UNSAFE, stringify } from '../src/index'
3+
import jsonWeb3, { parse, parse_UNSAFE, stringify, stringify_UNSAFE } from '../src/index'
44
import { getTypedArrayName, isBuffer } from '../src/utils'
55

66
describe('json-web3', () => {
@@ -176,7 +176,7 @@ describe('json-web3', () => {
176176
expect(output.d.getTime()).toBe(d.getTime())
177177
})
178178

179-
it('round-trips Map, Set, RegExp, URL, and Function with parse_UNSAFE', () => {
179+
it('round-trips Map, Set, RegExp, URL, and Function with stringify_UNSAFE + parse_UNSAFE', () => {
180180
function echo(arg: string) {
181181
return arg
182182
}
@@ -187,7 +187,7 @@ describe('json-web3', () => {
187187
url: new URL('https://example.com/'),
188188
fn: echo,
189189
}
190-
const text = stringify(input)
190+
const text = stringify_UNSAFE(input)
191191
const output = parse_UNSAFE(text)
192192

193193
expect(output.map).toBeInstanceOf(Map)
@@ -236,7 +236,7 @@ describe('json-web3', () => {
236236
url: new URL('https://example.com/'),
237237
fn: (arg: string) => arg,
238238
}
239-
const text = stringify(input, ['d', 'inf', 'map', 'set', 're', 'url', 'fn'])
239+
const text = stringify_UNSAFE(input, ['d', 'inf', 'map', 'set', 're', 'url', 'fn'])
240240
const output = parse_UNSAFE(text)
241241

242242
expect(output.d).toBeInstanceOf(Date)
@@ -415,10 +415,10 @@ describe('json-web3', () => {
415415
expect(output.bytes[256]).toBe(0)
416416
})
417417

418-
it('symbol is dropped while function is preserved with parse_UNSAFE', () => {
418+
it('symbol is dropped while function is preserved with stringify_UNSAFE + parse_UNSAFE', () => {
419419
const sym = Symbol('x')
420420
const input: any = { a: 1, s: sym, f: (arg: string) => arg }
421-
const text = stringify(input)
421+
const text = stringify_UNSAFE(input)
422422
const output = parse_UNSAFE(text)
423423
expect(output.a).toBe(1)
424424
expect(output.s).toBeUndefined()
@@ -428,12 +428,19 @@ describe('json-web3', () => {
428428

429429
it('parse keeps function payloads as tagged objects', () => {
430430
const input = { fn: (arg: string) => arg }
431-
const text = stringify(input)
431+
const text = stringify_UNSAFE(input)
432432
const output = parse(text)
433433

434434
expect(output.fn).toEqual({ '__@json.function__': expect.any(String) })
435435
})
436436

437+
it('stringify drops function by default', () => {
438+
const input = { fn: (arg: string) => arg }
439+
const text = stringify(input)
440+
const output = parse(text)
441+
expect(output).toEqual({})
442+
})
443+
437444
it('BigInt in array with replacer array filtering works as expected', () => {
438445
const input = { list: [1n, 2n], other: 3n }
439446
const text = stringify(input, ['list'])

test/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ const payload = {
1717

1818
const text = jsonWeb3.stringify(payload, null, 2)
1919
console.log('text', text)
20-
const restored = jsonWeb3.parse_UNSAFE(text)
20+
const restored = jsonWeb3.parse(text)
2121
console.log('restored', restored)

0 commit comments

Comments
 (0)