UNPKG

safe-json-value

Version:

⛑️ JSON serialization should never fail

162 lines (149 loc) 4.14 kB
/** * `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[] }