slimjson 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node *)",
5
+ "Bash(npx jest *)"
6
+ ]
7
+ }
8
+ }
package/README.md CHANGED
@@ -1,14 +1,17 @@
1
1
  # slimjson
2
2
 
3
+ 中文 | [English](./README_EN.md)
4
+
3
5
  轻量级对象数组压缩工具 — 将重复 key 的 JSON 对象数组转换为 `{ keys, rows }` 紧凑格式,并支持序列化时省略 `null` 以进一步减小体积。
4
6
 
5
7
  ## 适用场景
6
8
 
7
- - 后端返回列表接口时,每个对象都携带相同的 key 名,大量冗余
8
- - 不同对象可能拥有不同的字段(后端按需 omit null 字段)
9
- - 需要在网络传输中极致压缩 JSON 文本体积
9
+ - **API 列表接口**:后端返回列表接口时,每个对象都携带相同的 key 名,大量冗余
10
+ - **异构字段**:不同对象可能拥有不同的字段(后端按需 omit null 字段)
11
+ - **网络传输压缩**:需要在网络传输中极致压缩 JSON 文本体积
10
12
  - **大模型上下文压缩**:将大量结构化数据(如数据库查询结果、API 响应、知识库条目)压缩后送入 prompt,减少 token 消耗,降低调用成本
11
13
  - **大模型工具调用**:function calling / tool_use 返回的结果往往是结构化的对象数组,压缩后再回传给模型,可显著减少上下文窗口占用,让模型在有限 token 内处理更复杂的数据
14
+ - **大模型识别友好**:压缩后的 `{ keys, rows }` 格式将 schema(字段定义)与数据分离,key 只出现一次,模型能更准确地理解数据结构、按字段名提取信息,比重复 key 的原始 JSON 更不容易混淆
12
15
 
13
16
  ## 安装
14
17
 
@@ -18,12 +21,12 @@ npm install slimjson
18
21
 
19
22
  ## API
20
23
 
21
- ### `compress(source)`
24
+ ### `compress(source, opts?)`
22
25
 
23
26
  将对象数组压缩为 `{ keys, rows }` 结构:
24
27
 
25
28
  ```js
26
- const { compress } = require('slimjson');
29
+ import { compress } from 'slimjson';
27
30
 
28
31
  const users = [
29
32
  { name: 'Alice', age: 25, city: 'NYC' },
@@ -40,6 +43,14 @@ const compressed = compress(users);
40
43
  // }
41
44
  ```
42
45
 
46
+ **参数:**
47
+
48
+ | 参数 | 类型 | 默认值 | 说明 |
49
+ |------|------|--------|------|
50
+ | `source` | `Object[]` 或 `Object` | — | 待压缩的对象数组(单个对象会自动包裹为数组) |
51
+ | `opts` | `Object` | — | 可选配置 |
52
+ | `opts.trimTrailingNulls` | `boolean` | `false` | 是否去除行尾的 `null` 值 |
53
+
43
54
  **特点:**
44
55
  - `keys` 取所有对象的 key 并集,按首次出现顺序排列
45
56
  - 某对象缺失某字段 → 对应 row 位置填充 `null`
@@ -52,21 +63,46 @@ const compressed = compress(users);
52
63
  ```js
53
64
  const data = [
54
65
  { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello' } },
55
- { name: '李四', age: 35, profile: { avatar: 'b.jpg' } }, // 缺失 bio
66
+ { name: '李四', age: 35, profile: { avatar: 'b.jpg', file: null } },
67
+ { name: '王五' },
56
68
  ];
57
69
 
58
70
  compress(data);
59
71
  // {
60
- // keys: ['name', 'age', { profile: ['avatar', 'bio'] }],
72
+ // keys: ['name', 'age', { profile: ['avatar', 'bio', 'file'] }],
73
+ // rows: [
74
+ // ['张三', 28, ['a.jpg', 'Hello', null]],
75
+ // ['李四', 35, ['b.jpg', null, null]],
76
+ // ['王五', null, null]
77
+ // ]
78
+ // }
79
+ ```
80
+
81
+ #### `trimTrailingNulls`:去除尾部 null
82
+
83
+ 启用后,每行及嵌套子行尾部的 `null` 会被去除,进一步压缩体积:
84
+
85
+ ```js
86
+ compress(data, { trimTrailingNulls: true });
87
+ // {
88
+ // keys: ['name', 'age', { profile: ['avatar', 'bio', 'file'] }],
61
89
  // rows: [
62
90
  // ['张三', 28, ['a.jpg', 'Hello']],
63
- // ['李四', 35, ['b.jpg', null ]]
91
+ // ['李四', 35, ['b.jpg']],
92
+ // ['王五']
64
93
  // ]
65
94
  // }
95
+ ```
96
+
97
+ `decompress` 会自动将缺失的尾部值补回 `null`,roundtrip 还原结果一致:
66
98
 
67
- stringify(compress(data));
68
- // {keys:[name,age,{profile:[avatar,bio]}],rows:[[张三,28,[a.jpg,Hello]],[李四,35,[b.jpg,]]]}
69
- // ^^ 省略 null,保留逗号
99
+ ```js
100
+ decompress(compress(data, { trimTrailingNulls: true }));
101
+ // [
102
+ // { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello', file: null } },
103
+ // { name: '李四', age: 35, profile: { avatar: 'b.jpg', bio: null, file: null } },
104
+ // { name: '王五', age: null, profile: null }
105
+ // ]
70
106
  ```
71
107
 
72
108
  #### 对象数组示例(订单场景)
@@ -131,15 +167,33 @@ compress(orders);
131
167
  // }
132
168
  // specs 的 key 取并集:第一单有 layout,第二单有 size → 都保留,缺失的填 null
133
169
 
134
- stringify(compress(orders));
170
+ compress(orders, { trimTrailingNulls: true });
171
+ // rows 变为:
172
+ // [
173
+ // ['A001', '张三', [
174
+ // ['键盘', 299, ['黑色', '104键']],
175
+ // ['鼠标', 99, ['白色', null, '4000']]
176
+ // ]],
177
+ // ['A002', '李四', [
178
+ // ['显示器', 1999, ['银色']]
179
+ // ]]
180
+ // ]
181
+
182
+ stringify(compress(orders, { trimTrailingNulls: true }));
135
183
  // {keys:[orderId,customer,{items:[name,price,{specs:[color,layout,dpi,size]}]}],rows:[[
136
- // A001,张三,[[键盘,299,[黑色,104键,,]],[鼠标,99,[白色,,4000,]]]],[A002,李四,[[显示器,1999,[银色,,,27寸]]]]]}
137
- // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ specs 中缺失字段用空槽省略 null ^^^^^^^^^^^^^^^^^^^^^^^^
184
+ // A001,张三,[[键盘,299,[黑色,104]],[鼠标,99,[白色,,4000]]]],[A002,李四,[[显示器,1999,[银色]]]]]}
185
+ ```
186
+
187
+ #### 单对象示例
188
+ ```js
189
+ compress({ name: 'Alice', age: 25 });
190
+ // 等价于 compress([{ name: 'Alice', age: 25 }])
191
+ // { keys: ['name', 'age'], rows: [['Alice', 25]] }
138
192
  ```
139
193
 
140
194
  ### `decompress(compressed)`
141
195
 
142
- 将 `{ keys, rows }` 还原为原始对象数组:
196
+ 将 `{ keys, rows }` 还原为原始对象数组。缺失的尾部值会自动补回 `null`:
143
197
 
144
198
  ```js
145
199
  const restored = decompress(compressed);
@@ -148,7 +202,7 @@ const restored = decompress(compressed);
148
202
 
149
203
  ### `stringify(compressed)`
150
204
 
151
- 将 compress 结果序列化为文本,数组中 `null` 值被省略(保留逗号占位),安全的字符串省略引号:
205
+ 将 compress 结果序列化为紧凑文本。相比 `JSON.stringify`,应用了以下优化规则:
152
206
 
153
207
  ```js
154
208
  const data = [
@@ -158,23 +212,65 @@ const data = [
158
212
 
159
213
  const text = stringify(compress(data));
160
214
  // {keys:[name,age],rows:[[Alice,25],[Bob,30]]}
161
- ```
162
215
 
163
- 对比 JSON.stringify:
164
- ```js
165
216
  JSON.stringify(compress(data));
166
217
  // {"keys":["name","age"],"rows":[["Alice",25],["Bob",30]]}
167
- // ↑ 引号 ↑ ↑ 引号 ↑
168
218
  ```
169
219
 
170
- 数组 null 省略规则:
220
+ #### 序列化规则一览
221
+
222
+ | 值类型 | 序列化结果 | 说明 |
223
+ |--------|-----------|------|
224
+ | `null` / `undefined` | `null` | — |
225
+ | 有限数字 | `25` | 直接输出,无引号 |
226
+ | `NaN` / `Infinity` | `null` | 非有限数统一输出 null |
227
+ | `true` / `false` | `true` / `false` | — |
228
+ | 安全字符串 | `Alice` | 省略引号(见下方规则) |
229
+ | 非安全字符串 | `"hello world"` | 保留 JSON 引号和转义 |
230
+ | 嵌套对象 `{k: v}` | `{k:v}` | key 同样区分安全/非安全 |
231
+ | 数组 | 见下方 null 省略规则 | — |
232
+
233
+ #### 安全字符串(可省略引号的条件)
234
+
235
+ 满足以下**全部**条件的字符串可省略引号,否则保留 `JSON.stringify` 转义:
236
+
237
+ 1. 非空字符串
238
+ 2. 不是关键字字面量:`null`、`true`、`false`
239
+ 3. 不匹配数字模式:`/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/`(如 `"123"`、`"-3.14"`、`"1e10"` 均保留引号)
240
+ 4. 不以数字或减号 `-` 开头
241
+ 5. 不含空白、`[`、`]`、`{`、`}`、`,`、`:`、`"` 等字符
242
+
243
+ | 字符串 | 结果 | 原因 |
244
+ |--------|------|------|
245
+ | `"Alice"` | `Alice` | 安全,省略引号 |
246
+ | `"hello world"` | `"hello world"` | 含空格 |
247
+ | `"123"` | `"123"` | 看起来像数字 |
248
+ | `"-3.14"` | `"-3.14"` | 看起来像数字 |
249
+ | `"null"` | `"null"` | 关键字 |
250
+ | `""` | `""` | 空字符串 |
251
+ | `"-abc"` | `"-abc"` | 以减号开头 |
252
+ | `"a:b"` | `"a:b"` | 含冒号 |
253
+
254
+ #### 对象 key 引号规则
255
+
256
+ `keys` 中的嵌套对象 key 同样适用安全字符串判断:
257
+
258
+ ```js
259
+ stringify({ keys: [{ profile: ['name', 'age'] }], rows: [...] });
260
+ // {keys:[{profile:[name,age]}],rows:[...]} ← profile 是安全 key,省略引号
261
+
262
+ stringify({ keys: [{ "my-key": ['name'] }], rows: [...] });
263
+ // {keys:[{"my-key":[name]}],rows:[...]} ← my-key 含减号,保留引号
264
+ ```
265
+
266
+ #### 数组 null 省略规则
267
+
268
+ 数组中的 `null` / `undefined` 被省略为逗号空槽,不占文字体积:
171
269
 
172
270
  | 原始数组 | 序列化结果 | 说明 |
173
271
  |----------------------|------------|------|
174
272
  | `["a", null, null]` | `[a,,]` | 尾部两个空槽 |
175
273
  | `[null, 1, null]` | `[,1,]` | 前后空槽 |
176
- | `[null, "1", null]` | `[,"1",]` | 前后空槽 |
177
- | `[null, "1a", null]` | `[,"1a",]` | 前后空槽 |
178
274
  | `[]` | `[]` | 空数组 |
179
275
  | `[null]` | `[null]` | **特殊**:`[,]` 代表 2 个 null,因此单 null 保留文字 |
180
276
 
@@ -192,7 +288,7 @@ const parsed = parse(text);
192
288
  ## 完整使用示例
193
289
 
194
290
  ```js
195
- const { compress, decompress, stringify, parse } = require('slimjson');
291
+ import { compress, decompress, stringify, parse } from 'slimjson';
196
292
 
197
293
  const data = [
198
294
  { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello' } },
@@ -206,6 +302,11 @@ const parsed = parse(text);
206
302
  const restored = decompress(parsed);
207
303
 
208
304
  // restored 与 data 深度相等
305
+
306
+ // 启用 trimTrailingNulls 进一步压缩
307
+ const compressedTrim = compress(data, { trimTrailingNulls: true });
308
+ const textTrim = stringify(compressedTrim);
309
+ // textTrim 比 text 更短
209
310
  ```
210
311
 
211
312
  ### 压缩率计算
@@ -219,33 +320,35 @@ console.log(`压缩率: ${ratio}%`);
219
320
 
220
321
  ## 压缩效果
221
322
 
222
- 基于 `compress-test.js` 基准测试的实际数据(18 组测试,平均压缩率 **52.20%**,所有 roundtrip 解压正确):
323
+ 基于 `compress-test.js` 基准测试的实际数据(18 组测试,所有 roundtrip 解压正确):
223
324
 
224
- | 数据类型 | 对象数 | 原始大小 | 压缩后 | 压缩率 |
225
- |---------|-------|---------|-------|-------|
226
- | 简单用户 | 1,000 | 147.85 KB | 87.36 KB | **40.91%** |
227
- | 简单用户 | 10,000 | 1.45 MB | 882.34 KB | **40.69%** |
228
- | 嵌套用户(含 profile.social) | 1,000 | 235.28 KB | 153.03 KB | **34.96%** |
229
- | 订单(每单1-5商品) | 500 | 166.34 KB | 72.10 KB | **56.66%** |
230
- | 学校数据(6年级×4班×30生) | 24 | 215.47 KB | 88.39 KB | **58.98%** |
231
- | 稀疏字段(500条×30字段) | 500 | 144.68 KB | 45.39 KB | **68.62%** |
232
- | 稀疏字段(2000条×50字段) | 2,000 | 947.88 KB | 292.74 KB | **69.12%** |
233
- | 深层嵌套(5层组织结构) | 5 | 634.65 KB | 288.75 KB | **54.50%** |
325
+ | 数据类型 | 对象数 | 原始大小 | trim | 压缩率 | trim | 压缩率 | 差值 |
326
+ |---------|-------|---------|---------|-------|------|-------|------|
327
+ | 简单用户 | 1,000 | 147.61 KB | 87.12 KB | 40.98% | 87.12 KB | 40.98% | — |
328
+ | 简单用户 | 10,000 | 1.45 MB | 882.51 KB | 40.69% | 882.51 KB | 40.69% | — |
329
+ | 嵌套用户(含 profile.social) | 1,000 | 235.70 KB | 153.56 KB | 34.85% | 153.27 KB | 34.97% | -294 B |
330
+ | 订单(每单1-5商品) | 500 | 166.95 KB | 72.30 KB | 56.69% | 72.30 KB | 56.69% | — |
331
+ | 学校数据(6年级×4班×30生) | 24 | 214.86 KB | 88.88 KB | 58.63% | 88.53 KB | 58.80% | -365 B |
332
+ | 稀疏字段(500条×30字段) | 500 | 144.61 KB | 45.40 KB | 68.60% | 45.13 KB | 68.79% | -276 B |
333
+ | 稀疏字段(2000条×50字段) | 2,000 | 951.94 KB | 293.62 KB | 69.16% | 292.49 KB | 69.27% | -1.13 KB |
334
+ | 深层嵌套(5层组织结构) | 5 | 634.60 KB | 289.02 KB | 54.46% | 289.02 KB | 54.46% | — |
234
335
 
235
336
  **结论:**
236
337
  1. 字段名越长、数量越多,压缩效果越好
237
338
  2. 对象数组(订单条目、学生列表)压缩效果显著(55–59%)
238
339
  3. 稀疏字段压缩率最高 — 缺失字段的 null 通过空槽省略(67–69%)
239
- 4. 深层嵌套结构能获得更好的压缩效果
240
- 5. `stringify` 省略引号进一步减少文本体积
340
+ 4. `trimTrailingNulls` 在数据有缺失尾部字段时额外节省体积(最高 1.48 KB / 5000 条)
341
+ 5. 数据完整无缺失字段时,trim 无额外收益
342
+ 6. 深层嵌套结构能获得更好的压缩效果
343
+ 7. `stringify` 省略引号进一步减少文本体积
241
344
 
242
345
  ## 开发
243
346
 
244
347
  ```bash
245
- # 运行测试
348
+ # 运行测试(192 个用例,100% 覆盖率)
246
349
  npm test
247
350
 
248
- # 运行压缩率基准测试
351
+ # 运行压缩率基准测试(含 trim 对比)
249
352
  node compress-test.js
250
353
  ```
251
354
 
package/README_EN.md ADDED
@@ -0,0 +1,350 @@
1
+ # slimjson
2
+
3
+ [中文](./README.md) | English
4
+
5
+ A lightweight object array compression tool — converts JSON object arrays with repeated keys into a compact `{ keys, rows }` format, with support for omitting `null` values during serialization to further reduce size.
6
+
7
+ ## Use Cases
8
+
9
+ - **API List Endpoints**: Backend list endpoints where every object carries the same key names, resulting in massive redundancy
10
+ - **Heterogeneous Fields**: Objects with different fields (backend omits null fields on demand)
11
+ - **Network Transfer Compression**: Minimizing JSON text size for network transmission
12
+ - **LLM Context Compression**: Compress large structured data (e.g. database query results, API responses, knowledge base entries) before sending to prompts, reducing token consumption and API costs
13
+ - **LLM Tool Calling**: function calling / tool_use results are often structured object arrays — compressing them before feeding back to the model significantly reduces context window usage, enabling the model to handle more complex data within limited tokens
14
+ - **LLM-Friendly Format**: The compressed `{ keys, rows }` format separates schema (field definitions) from data, with each key appearing only once. Models can more accurately understand data structures and extract information by field name, with less confusion compared to raw JSON with repeated keys
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install slimjson
20
+ ```
21
+
22
+ ## API
23
+
24
+ ### `compress(source, opts?)`
25
+
26
+ Compresses an object array into a `{ keys, rows }` structure:
27
+
28
+ ```js
29
+ import { compress } from 'slimjson';
30
+
31
+ const users = [
32
+ { name: 'Alice', age: 25, city: 'NYC' },
33
+ { name: 'Bob', age: 30, city: 'LA' },
34
+ ];
35
+
36
+ const compressed = compress(users);
37
+ // {
38
+ // keys: ['name', 'age', 'city'],
39
+ // rows: [
40
+ // ['Alice', 25, 'NYC'],
41
+ // ['Bob', 30, 'LA' ]
42
+ // ]
43
+ // }
44
+ ```
45
+
46
+ **Parameters:**
47
+
48
+ | Parameter | Type | Default | Description |
49
+ |-----------|------|---------|-------------|
50
+ | `source` | `Object[]` or `Object` | — | Object array to compress (single object is auto-wrapped) |
51
+ | `opts` | `Object` | — | Optional configuration |
52
+ | `opts.trimTrailingNulls` | `boolean` | `false` | Remove trailing `null` values from each row |
53
+
54
+ **Features:**
55
+ - `keys` takes the union of all object keys, ordered by first appearance
56
+ - Missing fields in an object → fill `null` at the corresponding row position
57
+ - Nested objects are recursively processed: represented as `{ "fieldName": [childKeys] }` in `keys`
58
+ - Object arrays (e.g. order items) are recursively compressed the same way
59
+ - When a plain object is passed (not an array), it is treated as a single-element array
60
+
61
+ #### Nested Object Example
62
+
63
+ ```js
64
+ const data = [
65
+ { name: 'Alice', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello' } },
66
+ { name: 'Bob', age: 35, profile: { avatar: 'b.jpg', file: null } },
67
+ { name: 'Carol' },
68
+ ];
69
+
70
+ compress(data);
71
+ // {
72
+ // keys: ['name', 'age', { profile: ['avatar', 'bio', 'file'] }],
73
+ // rows: [
74
+ // ['Alice', 28, ['a.jpg', 'Hello', null]],
75
+ // ['Bob', 35, ['b.jpg', null, null]],
76
+ // ['Carol', null, null]
77
+ // ]
78
+ // }
79
+ ```
80
+
81
+ #### `trimTrailingNulls`: Remove Trailing nulls
82
+
83
+ When enabled, trailing `null` values in each row (and nested sub-rows) are removed for further compression:
84
+
85
+ ```js
86
+ compress(data, { trimTrailingNulls: true });
87
+ // {
88
+ // keys: ['name', 'age', { profile: ['avatar', 'bio', 'file'] }],
89
+ // rows: [
90
+ // ['Alice', 28, ['a.jpg', 'Hello']],
91
+ // ['Bob', 35, ['b.jpg']],
92
+ // ['Carol']
93
+ // ]
94
+ // }
95
+ ```
96
+
97
+ `decompress` automatically fills missing trailing values with `null`, so the roundtrip result is identical:
98
+
99
+ ```js
100
+ decompress(compress(data, { trimTrailingNulls: true }));
101
+ // [
102
+ // { name: 'Alice', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello', file: null } },
103
+ // { name: 'Bob', age: 35, profile: { avatar: 'b.jpg', bio: null, file: null } },
104
+ // { name: 'Carol', age: null, profile: null }
105
+ // ]
106
+ ```
107
+
108
+ #### Object Array Example (Order Scenario)
109
+
110
+ ```js
111
+ const orders = [
112
+ { orderId: 'A001', items: [{ name: 'Keyboard', price: 299 }, { name: 'Mouse', price: 99 }] },
113
+ { orderId: 'A002', items: [{ name: 'Monitor', price: 1999 }] },
114
+ ];
115
+
116
+ compress(orders);
117
+ // {
118
+ // keys: ['orderId', { items: ['name', 'price'] }],
119
+ // rows: [
120
+ // ['A001', [['Keyboard', 299], ['Mouse', 99]]],
121
+ // ['A002', [['Monitor', 1999]]]
122
+ // ]
123
+ // }
124
+
125
+ stringify(compress(orders));
126
+ // {keys:[orderId,{items:[name,price]}],rows:[[A001,[[Keyboard,299],[Mouse,99]]],[A002,[[Monitor,1999]]]]}
127
+ // ^^^^^ nested object key, no quotes ^^^^ safe string value, no quotes
128
+ ```
129
+
130
+ #### Three-Level Nesting Example (Order → Item → Specs)
131
+
132
+ ```js
133
+ const orders = [
134
+ {
135
+ orderId: 'A001',
136
+ customer: 'Alice',
137
+ items: [
138
+ { name: 'Keyboard', price: 299, specs: { color: 'Black', layout: '104-key' } },
139
+ { name: 'Mouse', price: 99, specs: { color: 'White', dpi: '4000' } },
140
+ ]
141
+ },
142
+ {
143
+ orderId: 'A002',
144
+ customer: 'Bob',
145
+ items: [
146
+ { name: 'Monitor', price: 1999, specs: { color: 'Silver', size: '27in' } },
147
+ ]
148
+ },
149
+ ];
150
+
151
+ compress(orders);
152
+ // {
153
+ // keys: [
154
+ // 'orderId',
155
+ // 'customer',
156
+ // { items: ['name', 'price', { specs: ['color', 'layout', 'dpi', 'size'] }] }
157
+ // ],
158
+ // rows: [
159
+ // ['A001', 'Alice', [
160
+ // ['Keyboard', 299, ['Black', '104-key', null, null]],
161
+ // ['Mouse', 99, ['White', null, '4000', null]]
162
+ // ]],
163
+ // ['A002', 'Bob', [
164
+ // ['Monitor', 1999, ['Silver', null, null, '27in']]
165
+ // ]]
166
+ // ]
167
+ // }
168
+ // specs keys take the union: order 1 has layout, order 2 has size → both kept, missing fields filled with null
169
+
170
+ compress(orders, { trimTrailingNulls: true });
171
+ // rows become:
172
+ // [
173
+ // ['A001', 'Alice', [
174
+ // ['Keyboard', 299, ['Black', '104-key']],
175
+ // ['Mouse', 99, ['White', null, '4000']]
176
+ // ]],
177
+ // ['A002', 'Bob', [
178
+ // ['Monitor', 1999, ['Silver']]
179
+ // ]]
180
+ // ]
181
+ ```
182
+
183
+ ### `decompress(compressed)`
184
+
185
+ Restores `{ keys, rows }` back to the original object array. Missing trailing values are automatically filled with `null`:
186
+
187
+ ```js
188
+ const restored = decompress(compressed);
189
+ // deep-equal to the original array
190
+ ```
191
+
192
+ ### `stringify(compressed)`
193
+
194
+ Serializes the compress result into compact text. Compared to `JSON.stringify`, the following optimization rules are applied:
195
+
196
+ ```js
197
+ const data = [
198
+ { name: 'Alice', age: 25 },
199
+ { name: 'Bob', age: 30 },
200
+ ];
201
+
202
+ const text = stringify(compress(data));
203
+ // {keys:[name,age],rows:[[Alice,25],[Bob,30]]}
204
+
205
+ JSON.stringify(compress(data));
206
+ // {"keys":["name","age"],"rows":[["Alice",25],["Bob",30]]}
207
+ ```
208
+
209
+ #### Serialization Rules
210
+
211
+ | Value Type | Serialized Result | Notes |
212
+ |------------|------------------|-------|
213
+ | `null` / `undefined` | `null` | — |
214
+ | Finite number | `25` | Direct output, no quotes |
215
+ | `NaN` / `Infinity` | `null` | Non-finite numbers unified to null |
216
+ | `true` / `false` | `true` / `false` | — |
217
+ | Safe string | `Alice` | Quotes omitted (see rules below) |
218
+ | Unsafe string | `"hello world"` | JSON quotes and escaping retained |
219
+ | Nested object `{k: v}` | `{k:v}` | Keys follow same safe/unsafe rules |
220
+ | Array | See null omission rules below | — |
221
+
222
+ #### Safe Strings (Conditions for Omitting Quotes)
223
+
224
+ A string can omit quotes only when it satisfies **all** of the following conditions; otherwise `JSON.stringify` escaping is applied:
225
+
226
+ 1. Non-empty string
227
+ 2. Not a keyword literal: `null`, `true`, `false`
228
+ 3. Does not match number pattern: `/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/` (e.g. `"123"`, `"-3.14"`, `"1e10"` all retain quotes)
229
+ 4. Does not start with a digit or minus sign `-`
230
+ 5. Does not contain whitespace, `[`, `]`, `{`, `}`, `,`, `:`, `"` or similar characters
231
+
232
+ | String | Result | Reason |
233
+ |--------|--------|--------|
234
+ | `"Alice"` | `Alice` | Safe, quotes omitted |
235
+ | `"hello world"` | `"hello world"` | Contains space |
236
+ | `"123"` | `"123"` | Looks like a number |
237
+ | `"-3.14"` | `"-3.14"` | Looks like a number |
238
+ | `"null"` | `"null"` | Keyword |
239
+ | `""` | `""` | Empty string |
240
+ | `"-abc"` | `"-abc"` | Starts with minus sign |
241
+ | `"a:b"` | `"a:b"` | Contains colon |
242
+
243
+ #### Object Key Quoting Rules
244
+
245
+ Nested object keys in `keys` follow the same safe string check:
246
+
247
+ ```js
248
+ stringify({ keys: [{ profile: ['name', 'age'] }], rows: [...] });
249
+ // {keys:[{profile:[name,age]}],rows:[...]} ← profile is safe, quotes omitted
250
+
251
+ stringify({ keys: [{ "my-key": ['name'] }], rows: [...] });
252
+ // {keys:[{"my-key":[name]}],rows:[...]} ← my-key contains hyphen, quotes retained
253
+ ```
254
+
255
+ #### Array Null Omission Rules
256
+
257
+ `null` / `undefined` values in arrays are omitted as comma slots, taking no text space:
258
+
259
+ | Original Array | Serialized Result | Notes |
260
+ |---------------|------------------|-------|
261
+ | `["a", null, null]` | `[a,,]` | Two trailing empty slots |
262
+ | `[null, 1, null]` | `[,1,]` | Leading and trailing empty slots |
263
+ | `[]` | `[]` | Empty array |
264
+ | `[null]` | `[null]` | **Special**: `[,]` means 2 nulls, so single null retains literal |
265
+
266
+ ### `parse(text)`
267
+
268
+ Parses text produced by `stringify`, restoring omitted `null` values:
269
+
270
+ ```js
271
+ const parsed = parse(text);
272
+ // deep-equal to compressed
273
+ ```
274
+
275
+ Supports full JSON type parsing (strings, numbers, booleans, null, nested objects/arrays), compatible with escape characters and Unicode.
276
+
277
+ ## Complete Example
278
+
279
+ ```js
280
+ import { compress, decompress, stringify, parse } from 'slimjson';
281
+
282
+ const data = [
283
+ { name: 'Alice', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello' } },
284
+ { name: 'Bob', age: 35, profile: { avatar: 'b.jpg' } }, // missing bio
285
+ ];
286
+
287
+ // Compress → Stringify → Parse → Decompress
288
+ const compressed = compress(data);
289
+ const text = stringify(compressed);
290
+ const parsed = parse(text);
291
+ const restored = decompress(parsed);
292
+
293
+ // restored is deep-equal to data
294
+
295
+ // Enable trimTrailingNulls for further compression
296
+ const compressedTrim = compress(data, { trimTrailingNulls: true });
297
+ const textTrim = stringify(compressedTrim);
298
+ // textTrim is shorter than text
299
+ ```
300
+
301
+ ### Compression Ratio Calculation
302
+
303
+ ```js
304
+ const originalSize = Buffer.byteLength(JSON.stringify(data));
305
+ const compressedSize = Buffer.byteLength(stringify(compress(data)));
306
+ const ratio = ((originalSize - compressedSize) / originalSize * 100).toFixed(1);
307
+ console.log(`Compression ratio: ${ratio}%`);
308
+ ```
309
+
310
+ ## Compression Results
311
+
312
+ Based on actual data from `compress-test.js` benchmarks (18 test cases, all roundtrip decompressions verified):
313
+
314
+ | Data Type | Count | Original | No trim | Ratio | Trim | Ratio | Diff |
315
+ |-----------|-------|----------|---------|-------|------|-------|------|
316
+ | Simple users | 1,000 | 147.61 KB | 87.12 KB | 40.98% | 87.12 KB | 40.98% | — |
317
+ | Simple users | 10,000 | 1.45 MB | 882.51 KB | 40.69% | 882.51 KB | 40.69% | — |
318
+ | Nested users (with profile.social) | 1,000 | 235.70 KB | 153.56 KB | 34.85% | 153.27 KB | 34.97% | -294 B |
319
+ | Orders (1-5 items per order) | 500 | 166.95 KB | 72.30 KB | 56.69% | 72.30 KB | 56.69% | — |
320
+ | School data (6 grades x 4 classes x 30 students) | 24 | 214.86 KB | 88.88 KB | 58.63% | 88.53 KB | 58.80% | -365 B |
321
+ | Sparse fields (500 records x 30 fields) | 500 | 144.61 KB | 45.40 KB | 68.60% | 45.13 KB | 68.79% | -276 B |
322
+ | Sparse fields (2000 records x 50 fields) | 2,000 | 951.94 KB | 293.62 KB | 69.16% | 292.49 KB | 69.27% | -1.13 KB |
323
+ | Deep nesting (5-level org structure) | 5 | 634.60 KB | 289.02 KB | 54.46% | 289.02 KB | 54.46% | — |
324
+
325
+ **Conclusions:**
326
+ 1. Longer field names and more fields yield better compression
327
+ 2. Object arrays (order items, student lists) show significant compression (55–59%)
328
+ 3. Sparse fields achieve the highest compression — missing field nulls omitted as empty slots (67–69%)
329
+ 4. `trimTrailingNulls` saves additional space when data has missing trailing fields (up to 1.48 KB / 5000 records)
330
+ 5. When data has no missing fields, trim provides no extra benefit
331
+ 6. Deeper nested structures achieve better compression
332
+ 7. `stringify` quote omission further reduces text size
333
+
334
+ ## Development
335
+
336
+ ```bash
337
+ # Run tests (192 cases, 100% coverage)
338
+ npm test
339
+
340
+ # Run compression ratio benchmarks (with trim comparison)
341
+ node compress-test.js
342
+ ```
343
+
344
+ ## GitHub
345
+
346
+ [https://github.com/LastHeaven/slimjson](https://github.com/LastHeaven/slimjson)
347
+
348
+ ## License
349
+
350
+ MIT