Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/ehmicky/safe-json-value

⛑️ JSON serialization should never fail
https://github.com/ehmicky/safe-json-value

bigint circular cycle enumerable exception-handling exceptions getters javascript json library nodejs parsing serialization symbols tojson types typescript valid validate validation

Last synced: 5 days ago
JSON representation

⛑️ JSON serialization should never fail

Awesome Lists containing this project

README

        


modern-errors logo

[![Node](https://img.shields.io/badge/-Node.js-808080?logo=node.js&colorA=404040&logoColor=66cc33)](https://www.npmjs.com/package/safe-json-value)
[![Browsers](https://img.shields.io/badge/-Browsers-808080?logo=firefox&colorA=404040)](https://unpkg.com/safe-json-value?module)
[![TypeScript](https://img.shields.io/badge/-Typed-808080?logo=typescript&colorA=404040&logoColor=0096ff)](/src/main.d.ts)
[![Codecov](https://img.shields.io/badge/-Tested%20100%25-808080?logo=codecov&colorA=404040)](https://codecov.io/gh/ehmicky/safe-json-value)
[![Minified size](https://img.shields.io/bundlephobia/minzip/safe-json-value?label&colorA=404040&colorB=808080&logo=webpack)](https://bundlephobia.com/package/safe-json-value)
[![Mastodon](https://img.shields.io/badge/-Mastodon-808080.svg?logo=mastodon&colorA=404040&logoColor=9590F9)](https://fosstodon.org/@ehmicky)
[![Medium](https://img.shields.io/badge/-Medium-808080.svg?logo=medium&colorA=404040)](https://medium.com/@ehmicky)

⛑️ JSON serialization should never fail.

# Hire me

Please
[reach out](https://www.linkedin.com/feed/update/urn:li:activity:7117265228068716545/)
if you're looking for a Node.js API or CLI engineer (11 years of experience).
Most recently I have been [Netlify Build](https://github.com/netlify/build)'s
and [Netlify Plugins](https://www.netlify.com/products/build/plugins/)'
technical lead for 2.5 years. I am available for full-time remote positions.

# Features

Prevent `JSON.stringify()` from:

- [Throwing](#exceptions)
- [Changing types](#unexpected-types)
- [Filtering](#filtered-values) or [transforming values](#unresolved-values)
unexpectedly

# Example

```js
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: { one: true, self: [Circular *1] },
// newValue: undefined,
// reason: 'unsafeCycle'
// }
// ]
```

# Install

```bash
npm install safe-json-value
```

This package works in both Node.js >=18.18.0 and
[browsers](https://raw.githubusercontent.com/ehmicky/dev-tasks/main/src/browserslist).

This is an ES module. It must be loaded using
[an `import` or `import()` statement](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c),
not `require()`. If TypeScript is used, it must be configured to
[output ES modules](https://www.typescriptlang.org/docs/handbook/esm-node.html),
not CommonJS.

# API

## safeJsonValue(value, options?)

`value` `any`\
`options` [`Options?`](#options)\
_Return value_: [`object`](#return-value)

Makes `value` [JSON-safe](#changes-1) by:

- Omitting properties which would [throw](#exceptions),
[change type unexpectedly](#unexpected-types) or
[be filtered](#filtered-values) with `JSON.stringify()`
- Resolving properties which would [change value](#unresolved-values) 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](#big-output). They are
completely removed, not truncated (including strings).

```js
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](#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](#changes-1) 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](#tojson).

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](#changes-1) applied to [`value`](#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`

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](#exceptions): [`"unsafeCycle"`](#cycles),
[`"unsafeBigInt"`](#bigint), [`"unsafeSize"`](#big-output),
[`"unsafeException"`](#infinite-recursion),
[`"unsafeToJSON"`](#exceptions-in-tojson),
[`"unsafeGetter"`](#exceptions-in-getters)
- [Invalid descriptors](#invalid-descriptors):
[`"descriptorNotWritable"`](#non-writable-properties),
[`"descriptorNotConfigurable"`](#non-configurable-properties)
- [Unexpected types](#unexpected-types):
[`"unstableInfinite"`](#nan-and-infinity)
- [Filtered values](#filtered-values): [`"ignoredFunction"`](#functions),
[`"ignoredUndefined"`](#undefined), [`"ignoredSymbolValue"`](#symbol-values),
[`"ignoredSymbolKey"`](#symbol-keys),
[`"ignoredNotEnumerable"`](#non-enumerable-keys),
[`"ignoredArrayProperty"`](#array-properties)
- [Unresolved values](#unresolved-values): [`"unresolvedToJSON"`](#tojson),
[`"unresolvedClass"`](#classes), [`"unresolvedGetter"`](#getters)

##### changes[*].error

_Type_: `Error?`

Error that triggered the change. Only present if [`reason`](#changesreason) is
[`"unsafeException"`](#infinite-recursion),
[`"unsafeToJSON"`](#exceptions-in-tojson) or
[`"unsafeGetter"`](#exceptions-in-getters).

# 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

```js
const input = { one: true }
input.self = input
JSON.stringify(input) // Throws due to cycle
JSON.stringify(safeJsonValue(input).value) // '{"one":true}"
```

### Infinite recursion

```js
const input = { toJSON: () => ({ one: true, input }) }
JSON.stringify(input) // Throws due to infinite `toJSON()` recursion
JSON.stringify(safeJsonValue(input).value) // '{"one":true,"input":{...}}"
```

### BigInt

```js
const input = { one: true, two: 0n }
JSON.stringify(input) // Throws due to BigInt
JSON.stringify(safeJsonValue(input).value) // '{"one":true}"
```

### Big output

```js
const input = { one: true, two: '\n'.repeat(5e8) }
JSON.stringify(input) // Throws due to max string length
JSON.stringify(safeJsonValue(input).value) // '{"one":true}"
```

### Exceptions in `toJSON()`

```js
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

```js
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

```js
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

```js
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

```js
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

```js
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

```js
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

```js
const input = { one: true, two: () => {} }
JSON.parse(JSON.stringify(input)) // { one: true }
safeJsonValue(input).value // { one: true }
```

### `undefined`

```js
const input = { one: true, two: undefined }
JSON.parse(JSON.stringify(input)) // { one: true }
safeJsonValue(input).value // { one: true }
```

### Symbol values

```js
const input = { one: true, two: Symbol() }
JSON.parse(JSON.stringify(input)) // { one: true }
safeJsonValue(input).value // { one: true }
```

### Symbol keys

```js
const input = { one: true, [Symbol()]: true }
JSON.parse(JSON.stringify(input)) // { one: true }
safeJsonValue(input).value // { one: true }
```

### Non-enumerable keys

```js
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

```js
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()`

```js
const input = {
toJSON: () => ({ one: true }),
}
JSON.parse(JSON.stringify(input)) // { one: true }
safeJsonValue(input).value // { one: true }
```

### Dates

```js
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

```js
const input = { one: new Set([]) }
JSON.parse(JSON.stringify(input)) // { one: {} }
safeJsonValue(input).value // { one: {} }
```

### Getters

```js
const input = {
get one() {
return true
},
}
JSON.parse(JSON.stringify(input)) // { one: true }
safeJsonValue(input).value // { one: true }
```

### Proxies

```js
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`](https://github.com/ehmicky/is-json-value): Check if a value
is valid JSON
- [`truncate-json`](https://github.com/ehmicky/truncate-json): Truncate a JSON
string
- [`guess-json-indent`](https://github.com/ehmicky/guess-json-indent): Guess the
indentation of a JSON string
- [`error-serializer`](https://github.com/ehmicky/error-serializer): Convert
errors to/from plain objects

# Support

For any question, _don't hesitate_ to [submit an issue on GitHub](../../issues).

Everyone is welcome regardless of personal background. We enforce a
[Code of conduct](CODE_OF_CONDUCT.md) 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](CONTRIBUTING.md). Pull requests are welcome!




ehmicky

💻 🎨 🤔 📖

Pedro Augusto de Paula Barbosa

📖