Skip to content

Commit f001e74

Browse files
committed
feat: add strict option
This option allows to verify that the serialized output fully reflects the input without loosing information.
1 parent 8e2dc7b commit f001e74

File tree

5 files changed

+271
-28
lines changed

5 files changed

+271
-28
lines changed

esm/wrapper.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface StringifyOptions {
77
deterministic?: boolean,
88
maximumBreadth?: number,
99
maximumDepth?: number,
10+
strict?: boolean,
1011
}
1112

1213
export namespace stringify {

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface StringifyOptions {
77
deterministic?: boolean,
88
maximumBreadth?: number,
99
maximumDepth?: number,
10+
strict?: boolean,
1011
}
1112

1213
export namespace stringify {

index.js

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict'
22

3+
const { hasOwnProperty } = Object.prototype
4+
35
const stringify = configure()
46

57
// @ts-expect-error
@@ -20,7 +22,7 @@ module.exports = stringify
2022
// eslint-disable-next-line
2123
const strEscapeSequencesRegExp = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]|[\ud800-\udbff](?![\udc00-\udfff])|(?:[^\ud800-\udbff]|^)[\udc00-\udfff]/
2224
// eslint-disable-next-line
23-
const strEscapeSequencesReplacer = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]|[\ud800-\udbff](?![\udc00-\udfff])|(?:[^\ud800-\udbff]|^)[\udc00-\udfff]/g
25+
const strEscapeSequencesReplacer = new RegExp(strEscapeSequencesRegExp, 'g')
2426

2527
// Escaped special characters. Use empty strings to fill up unused entries.
2628
const meta = [
@@ -69,13 +71,13 @@ function strEscape (str) {
6971
last = i + 1
7072
} else if (point >= 0xd800 && point <= 0xdfff) {
7173
if (point <= 0xdbff && i + 1 < str.length) {
72-
const point = str.charCodeAt(i + 1)
73-
if (point >= 0xdc00 && point <= 0xdfff) {
74+
const nextPoint = str.charCodeAt(i + 1)
75+
if (nextPoint >= 0xdc00 && nextPoint <= 0xdfff) {
7476
i++
7577
continue
7678
}
7779
}
78-
result += `${str.slice(last, i)}${`\\u${point.toString(16)}`}`
80+
result += `${str.slice(last, i)}\\u${point.toString(16)}`
7981
last = i + 1
8082
}
8183
}
@@ -105,7 +107,7 @@ const typedArrayPrototypeGetSymbolToStringTag =
105107
Object.getOwnPropertyDescriptor(
106108
Object.getPrototypeOf(
107109
Object.getPrototypeOf(
108-
new Uint8Array()
110+
new Int8Array()
109111
)
110112
),
111113
Symbol.toStringTag
@@ -128,8 +130,8 @@ function stringifyTypedArray (array, separator, maximumBreadth) {
128130
}
129131

130132
function getCircularValueOption (options) {
131-
if (options && Object.prototype.hasOwnProperty.call(options, 'circularValue')) {
132-
var circularValue = options.circularValue
133+
if (options && hasOwnProperty.call(options, 'circularValue')) {
134+
const circularValue = options.circularValue
133135
if (typeof circularValue === 'string') {
134136
return `"${circularValue}"`
135137
}
@@ -149,8 +151,9 @@ function getCircularValueOption (options) {
149151
}
150152

151153
function getBooleanOption (options, key) {
152-
if (options && Object.prototype.hasOwnProperty.call(options, key)) {
153-
var value = options[key]
154+
let value
155+
if (options && hasOwnProperty.call(options, key)) {
156+
value = options[key]
154157
if (typeof value !== 'boolean') {
155158
throw new TypeError(`The "${key}" argument must be of type boolean`)
156159
}
@@ -159,8 +162,9 @@ function getBooleanOption (options, key) {
159162
}
160163

161164
function getPositiveIntegerOption (options, key) {
162-
if (options && Object.prototype.hasOwnProperty.call(options, key)) {
163-
var value = options[key]
165+
let value
166+
if (options && hasOwnProperty.call(options, key)) {
167+
value = options[key]
164168
if (typeof value !== 'number') {
165169
throw new TypeError(`The "${key}" argument must be of type number`)
166170
}
@@ -184,16 +188,40 @@ function getItemCount (number) {
184188
function getUniqueReplacerSet (replacerArray) {
185189
const replacerSet = new Set()
186190
for (const value of replacerArray) {
187-
if (typeof value === 'string') {
188-
replacerSet.add(value)
189-
} else if (typeof value === 'number') {
191+
if (typeof value === 'string' || typeof value === 'number') {
190192
replacerSet.add(String(value))
191193
}
192194
}
193195
return replacerSet
194196
}
195197

198+
function getStrictOption (options) {
199+
if (options && hasOwnProperty.call(options, 'strict')) {
200+
const value = options.strict
201+
if (typeof value !== 'boolean') {
202+
throw new TypeError('The "strict" argument must be of type boolean')
203+
}
204+
if (value) {
205+
return (value) => {
206+
let message = `Object can not safely be stringified. Received type ${typeof value}`
207+
if (typeof value !== 'function') message += ` (${value.toString()})`
208+
throw new Error(message)
209+
}
210+
}
211+
}
212+
}
213+
196214
function configure (options) {
215+
options = { ...options }
216+
const fail = getStrictOption(options)
217+
if (fail) {
218+
if (options.bigint === undefined) {
219+
options.bigint = false
220+
}
221+
if (!('circularValue' in options)) {
222+
options.circularValue = Error
223+
}
224+
}
197225
const circularValue = getCircularValueOption(options)
198226
const bigint = getBooleanOption(options, 'bigint')
199227
const deterministic = getBooleanOption(options, 'deterministic')
@@ -302,11 +330,18 @@ function configure (options) {
302330
return `{${res}}`
303331
}
304332
case 'number':
305-
return isFinite(value) ? String(value) : 'null'
333+
return isFinite(value) ? String(value) : fail ? fail(value) : 'null'
306334
case 'boolean':
307335
return value === true ? 'true' : 'false'
336+
case 'undefined':
337+
return undefined
308338
case 'bigint':
309-
return bigint ? String(value) : undefined
339+
if (bigint) {
340+
return String(value)
341+
}
342+
// fallthrough
343+
default:
344+
return fail ? fail(value) : undefined
310345
}
311346
}
312347

@@ -387,11 +422,18 @@ function configure (options) {
387422
return `{${res}}`
388423
}
389424
case 'number':
390-
return isFinite(value) ? String(value) : 'null'
425+
return isFinite(value) ? String(value) : fail ? fail(value) : 'null'
391426
case 'boolean':
392427
return value === true ? 'true' : 'false'
428+
case 'undefined':
429+
return undefined
393430
case 'bigint':
394-
return bigint ? String(value) : undefined
431+
if (bigint) {
432+
return String(value)
433+
}
434+
// fallthrough
435+
default:
436+
return fail ? fail(value) : undefined
395437
}
396438
}
397439

@@ -490,11 +532,18 @@ function configure (options) {
490532
return `{${res}}`
491533
}
492534
case 'number':
493-
return isFinite(value) ? String(value) : 'null'
535+
return isFinite(value) ? String(value) : fail ? fail(value) : 'null'
494536
case 'boolean':
495537
return value === true ? 'true' : 'false'
538+
case 'undefined':
539+
return undefined
496540
case 'bigint':
497-
return bigint ? String(value) : undefined
541+
if (bigint) {
542+
return String(value)
543+
}
544+
// fallthrough
545+
default:
546+
return fail ? fail(value) : undefined
498547
}
499548
}
500549

@@ -583,11 +632,18 @@ function configure (options) {
583632
return `{${res}}`
584633
}
585634
case 'number':
586-
return isFinite(value) ? String(value) : 'null'
635+
return isFinite(value) ? String(value) : fail ? fail(value) : 'null'
587636
case 'boolean':
588637
return value === true ? 'true' : 'false'
638+
case 'undefined':
639+
return undefined
589640
case 'bigint':
590-
return bigint ? String(value) : undefined
641+
if (bigint) {
642+
return String(value)
643+
}
644+
// fallthrough
645+
default:
646+
return fail ? fail(value) : undefined
591647
}
592648
}
593649

readme.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# safe-stable-stringify
22

3-
Safe, deterministic and fast serialization alternative to [JSON.stringify][]. Zero dependencies. ESM and CJS. 100% coverage.
3+
Safe, deterministic and fast serialization alternative to [JSON.stringify][].
4+
Zero dependencies. ESM and CJS. 100% coverage.
45

56
Gracefully handles circular structures and bigint instead of throwing.
67

7-
Optional custom circular values and deterministic behavior.
8+
Optional custom circular values, deterministic behavior or strict JSON
9+
compatibility check.
810

911
## stringify(value[, replacer[, space]])
1012

@@ -47,7 +49,7 @@ stringify(circular, ['a', 'b'], 2)
4749
* `circularValue` {string|null|undefined|ErrorConstructor} Defines the value for
4850
circular references. Set to `undefined`, circular properties are not
4951
serialized (array entries are replaced with `null`). Set to `Error`, to throw
50-
on circular references. **Default:** `[Circular]`.
52+
on circular references. **Default:** `'[Circular]'`.
5153
* `deterministic` {boolean} If `true`, guarantee a deterministic key order
5254
instead of relying on the insertion order. **Default:** `true`.
5355
* `maximumBreadth` {number} Maximum number of entries to serialize per object
@@ -58,6 +60,10 @@ stringify(circular, ['a', 'b'], 2)
5860
* `maximumDepth` {number} Maximum number of object nesting levels (at least 1)
5961
that will be serialized. Objects at the maximum level are serialized as
6062
`'[Object]'` and arrays as `'[Array]'`. **Default:** `Infinity`
63+
* `strict` {boolean} Instead of handling any JSON value gracefully, throw an
64+
error in case it may not be represented as JSON (functions, NaN, ...).
65+
Circular values and bigint values throw as well in case either option is not
66+
explicitly defined. Sets and Maps are not detected! **Default:** `false`
6167
* Returns: {function} A stringify function with the options applied.
6268

6369
```js
@@ -101,9 +107,10 @@ throwOnCircular(circular);
101107

102108
## Differences to JSON.stringify
103109

104-
1. _Circular values_ are replaced with the string `[Circular]` (the value may be changed).
105-
1. _Object keys_ are sorted instead of using the insertion order (it is possible to deactivate this).
106-
1. _BigInt_ values are stringified as regular number instead of throwing a TypeError.
110+
1. _Circular values_ are replaced with the string `[Circular]` (configurable).
111+
1. _Object keys_ are sorted instead of using the insertion order (configurable).
112+
1. _BigInt_ values are stringified as regular number instead of throwing a
113+
TypeError (configurable).
107114
1. _Boxed primitives_ (e.g., `Number(5)`) are not unboxed and are handled as
108115
regular object.
109116

0 commit comments

Comments
 (0)