⛑️ JSON serialization should never fail.
Features
Prevent JSON.stringify()
from:
- Throwing
- Changing types
- Filtering or transforming values unexpectedly
Example
import safeJsonValue from 'safe-json-value'
const input = { one: true }
input.self = input
JSON.stringify(input) // Throws due to cycle
const { value, changes } = safeJsonValue(input)
JSON.stringify(value) // '{"one":true}"
console.log(changes) // List of changed properties
// [
// {
// path: ['self'],
// oldValue: <ref *1> { one: true, self: [Circular *1] },
// newValue: undefined,
// reason: 'unsafeCycle'
// }
// ]
Install
npm install safe-json-value
This package works in both Node.js >=16.17.0 and browsers.
This is an ES module. It must be loaded using
an import
or import()
statement,
not require()
. If TypeScript is used, it must be configured to
output ES modules,
not CommonJS.
API
safeJsonValue(value, options?)
value
any
options
Options?
Return value: object
Makes value
JSON-safe by:
- Omitting properties which would throw,
change type unexpectedly or
be filtered with
JSON.stringify()
- Resolving properties which would change value with
JSON.stringify()
This never throws.
Options
Object with the following properties.
maxSize
Type: number
Default: 1e7
Big JSON strings can make a process, filesystem operation or network request
crash. maxSize
prevents it by setting a maximum
JSON.stringify(value).length
.
Additional properties beyond the size limit are omitted. They are completely removed, not truncated (including strings).
const input = { one: true, two: 'a'.repeat(1e6) }
JSON.stringify(safeJsonValue(input, { maxSize: 1e5 }).value) // '{"one":true}"
shallow
Type: boolean
Default: false
If false
, object/array properties are processed recursively. Please note that
cycles are not removed when this is true
.
Return value
Object with the following properties.
value
Type: any
Copy of the input value
after applying all the changes to make
it JSON-safe.
The top-level value
itself might be changed (including to undefined
) if it
is either invalid JSON or has a toJSON()
method.
The value
is not serialized to a JSON string. This allows choosing the
serialization format (JSON, YAML, etc.), processing the value, etc.
changes
Type: Change[]
List of changes applied to value
. Each item is an
individual change to a specific property. A given property might have multiple
changes, listed in order.
changes[*].path
Type: Array<string | symbol | number>
Property path.
changes[*].oldValue
Type: any
Property value before the change.
changes[*].newValue
Type: any
Property value after the change. undefined
means the property was omitted.
changes[*].reason
Type: string
Reason for the change among:
- Exceptions:
"unsafeCycle"
,"unsafeBigInt"
,"unsafeSize"
,"unsafeException"
,"unsafeToJSON"
,"unsafeGetter"
- Invalid descriptors:
"descriptorNotWritable"
,"descriptorNotConfigurable"
- Unexpected types:
"unstableInfinite"
- Filtered values:
"ignoredFunction"
,"ignoredUndefined"
,"ignoredSymbolValue"
,"ignoredSymbolKey"
,"ignoredNotEnumerable"
,"ignoredArrayProperty"
- Unresolved values:
"unresolvedToJSON"
,"unresolvedClass"
,"unresolvedGetter"
changes[*].error
Type: Error?
Error that triggered the change. Only present if reason
is
"unsafeException"
,
"unsafeToJSON"
or
"unsafeGetter"
.
Changes
This is a list of all possible changes applied to make the value JSON-safe.
Exceptions
JSON.stringify()
can throw on specific properties. Those are omitted.
Cycles
const input = { one: true }
input.self = input
JSON.stringify(input) // Throws due to cycle
JSON.stringify(safeJsonValue(input).value) // '{"one":true}"
Infinite recursion
const input = { toJSON: () => ({ one: true, input }) }
JSON.stringify(input) // Throws due to infinite `toJSON()` recursion
JSON.stringify(safeJsonValue(input).value) // '{"one":true,"input":{...}}"
BigInt
const input = { one: true, two: 0n }
JSON.stringify(input) // Throws due to BigInt
JSON.stringify(safeJsonValue(input).value) // '{"one":true}"
Big output
const input = { one: true, two: '\n'.repeat(5e8) }
JSON.stringify(input) // Throws due to max string length
JSON.stringify(safeJsonValue(input).value) // '{"one":true}"
toJSON()
Exceptions in const input = {
one: true,
two: {
toJSON: () => {
throw new Error('example')
},
},
}
JSON.stringify(input) // Throws due to `toJSON()`
JSON.stringify(safeJsonValue(input).value) // '{"one":true}"
Exceptions in getters
const input = {
one: true,
get two() {
throw new Error('example')
},
}
JSON.stringify(input) // Throws due to `get two()`
JSON.stringify(safeJsonValue(input).value) // '{"one":true}"
Exceptions in proxies
const input = new Proxy(
{ one: false },
{
get: () => {
throw new Error('example')
},
},
)
JSON.stringify(input) // Throws due to proxy
JSON.stringify(safeJsonValue(input).value) // '{}'
Invalid descriptors
Non-writable properties
const input = {}
Object.defineProperty(input, 'one', {
value: true,
enumerable: true,
writable: false,
configurable: true,
})
input.one = false // Throws: non-writable
const safeInput = safeJsonValue(input).value
safeInput.one = false // Does not throw: now writable
Non-configurable properties
const input = {}
Object.defineProperty(input, 'one', {
value: true,
enumerable: true,
writable: true,
configurable: false,
})
// Throws: non-configurable
Object.defineProperty(input, 'one', { value: false, enumerable: false })
const safeInput = safeJsonValue(input).value
// Does not throw: now configurable
Object.defineProperty(safeInput, 'one', { value: false, enumerable: false })
Unexpected types
JSON.stringify()
changes the types of specific values unexpectedly. Those are
omitted.
NaN and Infinity
const input = { one: true, two: Number.NaN, three: Number.POSITIVE_INFINITY }
JSON.stringify(input) // '{"one":true,"two":null,"three":null}"
JSON.stringify(safeJsonValue(input).value) // '{"one":true}"
Invalid array items
const input = [true, undefined, Symbol(), false]
JSON.stringify(input) // '[true, null, null, false]'
JSON.stringify(safeJsonValue(input).value) // '[true, false]'
Filtered values
JSON.stringify()
omits some specific types. Those are omitted right away to
prevent any unexpected output.
Functions
const input = { one: true, two: () => {} }
JSON.parse(JSON.stringify(input)) // { one: true }
safeJsonValue(input).value // { one: true }
undefined
const input = { one: true, two: undefined }
JSON.parse(JSON.stringify(input)) // { one: true }
safeJsonValue(input).value // { one: true }
Symbol values
const input = { one: true, two: Symbol() }
JSON.parse(JSON.stringify(input)) // { one: true }
safeJsonValue(input).value // { one: true }
Symbol keys
const input = { one: true, [Symbol()]: true }
JSON.parse(JSON.stringify(input)) // { one: true }
safeJsonValue(input).value // { one: true }
Non-enumerable keys
const input = { one: true }
Object.defineProperty(input, 'two', { value: true, enumerable: false })
JSON.parse(JSON.stringify(input)) // { one: true }
safeJsonValue(input).value // { one: true }
Array properties
const input = [true]
input.prop = true
JSON.parse(JSON.stringify(input)) // [true]
safeJsonValue(input).value // [true]
Unresolved values
JSON.stringify()
can transform some values. Those are resolved right away to
prevent any unexpected output.
toJSON()
const input = {
toJSON: () => ({ one: true }),
}
JSON.parse(JSON.stringify(input)) // { one: true }
safeJsonValue(input).value // { one: true }
Dates
const input = { one: new Date() }
JSON.parse(JSON.stringify(input)) // { one: '2022-07-29T14:37:40.865Z' }
safeJsonValue(input).value // { one: '2022-07-29T14:37:40.865Z' }
Classes
const input = { one: new Set([]) }
JSON.parse(JSON.stringify(input)) // { one: {} }
safeJsonValue(input).value // { one: {} }
Getters
const input = {
get one() {
return true
},
}
JSON.parse(JSON.stringify(input)) // { one: true }
safeJsonValue(input).value // { one: true }
Proxies
const input = new Proxy(
{ one: false },
{
get: () => true,
},
)
JSON.parse(JSON.stringify(input)) // { one: true }
safeJsonValue(input).value // { one: true }
Related projects
is-json-value
: Check if a value is valid JSONtruncate-json
: Truncate a JSON stringguess-json-indent
: Guess the indentation of a JSON stringerror-serializer
: Convert errors to/from plain objects
Support
For any question, don't hesitate to submit an issue on GitHub.
Everyone is welcome regardless of personal background. We enforce a Code of conduct in order to promote a positive and inclusive environment.
Contributing
This project was made with ❤️. The simplest way to give back is by starring and sharing it online.
If the documentation is unclear or has a typo, please click on the page's Edit
button (pencil icon) and suggest a correction.
If you would like to help us fix a bug or add a new feature, please check our guidelines. Pull requests are welcome!
ehmicky 💻 🎨 🤔 📖 |
Pedro Augusto de Paula Barbosa 📖 |