safe-json-value
Version:
⛑️ JSON serialization should never fail
162 lines (149 loc) • 4.14 kB
TypeScript
/**
* `safe-json-value` options
*/
export interface Options {
/**
* 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).
*
* @default 1e7
*
* @example
* ```js
* const input = { one: true, two: 'a'.repeat(1e6) }
* JSON.stringify(safeJsonValue(input, { maxSize: 1e5 }).value) // '{"one":true}"
* ```
*/
readonly maxSize?: number
/**
* If `false`, object/array properties are processed recursively.
* Please note that cycles are not removed when this is `true`.
*
* @default false
*/
readonly shallow?: boolean
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
type InvalidJSONValue = bigint | Function | undefined | symbol
type ReturnValue<T, Shallow extends boolean> = T extends (infer ArrayItem)[]
? Shallow extends true
? ArrayItem[]
: ReturnValue<ArrayItem, Shallow>[]
: T extends InvalidJSONValue
? undefined
: T extends Date
? string
: T extends { toJSON: () => unknown }
? ReturnType<T['toJSON']>
: T extends object
? {
[key in keyof T as T[key] extends InvalidJSONValue
? never
: Exclude<key, symbol>]?: Shallow extends true
? T[key]
: ReturnValue<T[key], Shallow>
}
: T
type ReasonWithError = 'unsafeException' | 'unsafeGetter' | 'unsafeToJSON'
type ReasonWithoutError =
| 'descriptorNotConfigurable'
| 'descriptorNotWritable'
| 'ignoredArrayProperty'
| 'ignoredFunction'
| 'ignoredNotEnumerable'
| 'ignoredSymbolKey'
| 'ignoredSymbolValue'
| 'ignoredUndefined'
| 'unresolvedClass'
| 'unresolvedGetter'
| 'unresolvedToJSON'
| 'unsafeBigInt'
| 'unsafeCycle'
| 'unsafeSize'
| 'unstableInfinite'
/**
* Reason why a property was changed.
*/
export type Reason = ReasonWithError | ReasonWithoutError
/**
* Change applied to [`value`](#value).
* Each item is an individual change to a specific property.
* A given property might have multiple changes, listed in order.
*/
export type Change<ReasonValue extends Reason = Reason> = {
/**
* Property path.
*/
path: PropertyKey[]
/**
* Property value before the change.
*/
oldValue: unknown
/**
* Property value after the change.
* `undefined` means the property was omitted.
*/
newValue: unknown
/**
* Reason for the change.
*/
reason: ReasonValue
} & (ReasonValue extends ReasonWithError
? {
/**
* Error that triggered the change.
*/
error: Error
}
: 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()`
*
* Applied recursively on object/array properties. This never throws.
*
* @example
* ```js
* 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'
* // }
* // ]
* ```
*/
export default function safeJsonValue<T, OptionsArg extends Options = object>(
value: T,
options?: OptionsArg,
): {
/**
* 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.
*/
value:
| ReturnValue<T, OptionsArg['shallow'] extends true ? true : false>
| undefined
/**
* List of changes applied to `value`.
*/
changes: Change[]
}