Skip to content

Commit 9dabf67

Browse files
committed
docs: ✏️ Add RFC.md
1 parent ba704e4 commit 9dabf67

4 files changed

Lines changed: 180 additions & 1 deletion

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,9 @@ window.JSON = jsonWeb3 // Yes, you can replace it directly; it is fully compatib
128128
- 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`).
129129

130130
Compared to libraries that require eval-based parsing (for example, `serialize-javascript`), this approach is generally safer and more efficient.
131+
132+
## RFC (Serialization Format)
133+
134+
To enable cross-language interoperability, the serialization format is specified
135+
in [RFC.md](RFC.md). It defines the canonical tag objects, payload shapes, validation
136+
rules, and UNSAFE function handling.

RFC.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
RFC: JSON-WEB3 Serialization Format
2+
Status: Draft
3+
Version: 1
4+
5+
Abstract
6+
This document specifies a JSON-compatible serialization format used by json-web3
7+
to encode data types that are not supported by standard JSON. The goal is to
8+
enable interoperability across languages by defining canonical tag objects and
9+
payload shapes.
10+
11+
Terminology
12+
13+
- "Tag object" means a JSON object with exactly one of the reserved keys defined
14+
in this document.
15+
- "Canonical encoding" means the form produced by the reference implementation.
16+
- "Producer" means a serializer that emits this format.
17+
- "Consumer" means a parser that restores values from this format.
18+
19+
1. Data Model
20+
The format is a superset of JSON that preserves:
21+
22+
- BigInt
23+
- Non-finite numbers (NaN, Infinity, -Infinity)
24+
- Date
25+
- RegExp
26+
- URL
27+
- Map
28+
- Set
29+
- ArrayBuffer
30+
- TypedArray (including BigInt typed arrays)
31+
- Optional: Function (UNSAFE mode only)
32+
33+
2. Reserved Tag Keys
34+
All tag keys are strings that MUST be used exactly as written:
35+
36+
- "__@json.bigint__"
37+
- "__@json.number__"
38+
- "__@json.date__"
39+
- "__@json.regexp__"
40+
- "__@json.url__"
41+
- "__@json.map__"
42+
- "__@json.set__"
43+
- "__@json.typedarray__"
44+
- "__@json.arraybuffer__"
45+
- "__@json.function__"
46+
47+
3. Encoding Rules (Canonical)
48+
Producers MUST output one of the following tag objects when the source value
49+
matches the type. All tag objects SHOULD have no other properties.
50+
51+
3.1 BigInt
52+
{"__@json.bigint__": "<decimal-string>"}
53+
The decimal string MUST be base-10 with optional leading "-" for negative
54+
values. No "+" prefix.
55+
56+
3.2 Non-finite Number
57+
{"__@json.number__": "NaN" | "Infinity" | "-Infinity"}
58+
Finite numbers MUST be encoded as standard JSON numbers, but a producer MUST
59+
reject (or error on) any finite number that exceeds the safe integer range
60+
[-9007199254740991, 9007199254740991].
61+
62+
3.3 Date
63+
{"__@json.date__": <milliseconds-since-epoch>}
64+
The value is a JSON number representing milliseconds since Unix epoch.
65+
66+
3.4 RegExp
67+
{"__@json.regexp__": {"source": "<pattern>", "flags": "<flags>"}}
68+
Both "source" and "flags" MUST be strings.
69+
70+
3.5 URL
71+
{"__@json.url__": "<url-string>"}
72+
73+
3.6 Map
74+
{"__@json.map__": [[k1, v1], [k2, v2], ...]}
75+
The payload MUST be a JSON array of 2-element arrays. Order is preserved.
76+
77+
3.7 Set
78+
{"__@json.set__": [v1, v2, ...]}
79+
Order is preserved.
80+
81+
3.8 TypedArray (and Node.js Buffer JSON shape)
82+
{"__@json.typedarray__": {"type": "<CtorName>", "bytes": "0x..."}}
83+
84+
The "type" field MUST be one of:
85+
Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
86+
Int32Array, Uint32Array, Float16Array, Float32Array, Float64Array,
87+
BigInt64Array, BigUint64Array
88+
89+
"bytes" MUST be a hexadecimal string with a "0x" prefix. Canonical encoding
90+
uses lowercase hex. The byte sequence is the raw bytes of the view (respecting
91+
byteOffset and byteLength), not the full underlying buffer.
92+
93+
Node.js Buffer JSON shape is also accepted and normalized:
94+
{"type":"Buffer","data":[0..255]} -> encoded as type "Uint8Array".
95+
96+
3.9 ArrayBuffer
97+
{"__@json.arraybuffer__": {"bytes": "0x..."}}
98+
Same hex format as TypedArray bytes.
99+
100+
3.10 Function (UNSAFE only)
101+
{"__@json.function__": "<source>"}
102+
The source is the function's string representation. Consumers MAY refuse to
103+
revive functions unless explicitly running in UNSAFE mode.
104+
105+
4. Decoding Rules
106+
Consumers MUST:
107+
108+
- Detect any tag object and reconstruct the corresponding type.
109+
- Validate the payload structure and types, otherwise raise an error.
110+
- For __@json.number__, reject any string other than NaN, Infinity, -Infinity.
111+
- For __@json.typedarray__, if the "type" is unknown, decode as Uint8Array
112+
using the provided bytes.
113+
114+
5. Collision and Compatibility
115+
Because tag objects are plain JSON objects, a producer SHOULD avoid emitting
116+
regular objects that use any reserved tag key with a single-property object
117+
shape, to prevent accidental decoding.
118+
119+
6. Security Considerations
120+
121+
- The __@json.function__ tag can execute arbitrary code if revived. Only enable
122+
UNSAFE mode for trusted inputs.
123+
- Non-finite numbers and BigInt are represented as strings; treat them as
124+
untrusted input and validate if used in critical logic.
125+
126+
7. IANA Considerations
127+
None.
128+
129+
8. Reference Implementation
130+
json-web3 (TypeScript) is the reference implementation of this specification.

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.3.4",
3+
"version": "1.3.5",
44
"description": "BigInt-safe JSON serialization and deserialization for Web3 use cases.",
55
"keywords": [
66
"json",

test/index.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ describe('json-web3', () => {
5454
expect(Array.from(output.data)).toEqual([1, 2, 3, 255])
5555
})
5656

57+
it('serializes typed array view using byteOffset/byteLength', () => {
58+
const buffer = new ArrayBuffer(6)
59+
new Uint8Array(buffer).set([1, 2, 3, 4, 5, 6])
60+
const view = new Uint8Array(buffer, 2, 3) // [3,4,5]
61+
62+
const text = stringify({ data: view })
63+
const output = parse(text)
64+
65+
expect(text).toContain('0x030405')
66+
expect(output.data).toBeInstanceOf(Uint8Array)
67+
expect(Array.from(output.data)).toEqual([3, 4, 5])
68+
})
69+
5770
it('serializes cross-realm Uint8Array bytes', () => {
5871
const context = createContext({})
5972
const script = new Script('u = new Uint8Array([1, 2, 3, 255])')
@@ -130,6 +143,18 @@ describe('json-web3', () => {
130143
expect(Array.from(new Uint8Array(output.ab))).toEqual([9, 8, 7, 6])
131144
})
132145

146+
it('serializes ArrayBuffer bytes exactly', () => {
147+
const ab = new ArrayBuffer(3)
148+
new Uint8Array(ab).set([10, 11, 12])
149+
150+
const text = stringify({ ab })
151+
const output = parse(text)
152+
153+
expect(text).toContain('0x0a0b0c')
154+
expect(output.ab).toBeInstanceOf(ArrayBuffer)
155+
expect(Array.from(new Uint8Array(output.ab))).toEqual([10, 11, 12])
156+
})
157+
133158
it('does not mutate JSON primitives and null', () => {
134159
const input = {
135160
s: 'x',
@@ -210,6 +235,24 @@ describe('json-web3', () => {
210235
expect(output.fn('ok')).toBe('ok')
211236
})
212237

238+
it('preserves Map/Set insertion order', () => {
239+
const input = {
240+
map: new Map([
241+
['b', 2],
242+
['a', 1],
243+
]),
244+
set: new Set([3, 1, 2]),
245+
}
246+
const text = stringify(input)
247+
const output = parse(text)
248+
249+
expect(Array.from(output.map.entries())).toEqual([
250+
['b', 2],
251+
['a', 1],
252+
])
253+
expect(Array.from(output.set.values())).toEqual([3, 1, 2])
254+
})
255+
213256
it('supports space argument formatting', () => {
214257
const text = stringify({ a: 1, b: 2 }, null, 2)
215258
expect(text).toContain('\n')

0 commit comments

Comments
 (0)