lossless-json

repository·main·Indexed 19 days ago

https://github.com/josdejong/lossless-json

A JavaScript library (v4.3.1) designed to parse and stringify JSON without losing numeric precision. It prevents the data corruption common with native JSON.parse when handling large integers, high-precision decimals, or extreme exponents by using a LosslessNumber class. It supports custom numberParsers for BigInt integration, provides utilities for validating safe numbers, and includes a reviveDate helper for ISO 8601 strings.

Tokens
9.2K
Snippets
40
Records
43
Agent score
65%

What's inside lossless-json

  1. How LosslessNumbers work

    main

    By default, numeric values are parsed into a LosslessNumber class. This class stores the value as a string to prevent precision loss.

    • You can perform regular numeric operations with a LosslessNumber (e.g., json.number + 2).
    • You can check if a value is a lossless number using the .isLosslessNumber property.
    • You can convert it to a native number using .valueOf().
    • Warning: Converting a LosslessNumber to a native number will throw an error if the conversion would result in information loss (truncation, overflow, or underflow).
    import { parse } from 'lossless-json'
    
    const text = '{"normal":2.3,"long":123456789012345678901,"big":2.3e+500}'
    const json = parse(text)
    
    console.log(json.normal.isLosslessNumber) // true
    console.log(json.normal.valueOf()) // number, 2.3
    
    // LosslessNumbers can be used as regular numbers
    console.log(json.normal + 2) // number, 4.3
    
    // This will throw an error because it would lose information:
    console.log(json.long + 1)
    // throws Error: Cannot safely convert LosslessNumber to number...
  2. Build the bundled and minified library

    main

    To generate a bundled and minified ES5 library (including an ES module and a UMD bundle for browsers and Node.js), follow these steps:

    1. Install dependencies: npm install
    2. Run the build command: npm run build

    The output will be located in the ./.lib folder.

    npm install
    npm run build
  3. Release a new version

    main

    To release a new version of the package, use the release command. This automation performs linting, testing, building, version incrementing, git tagging, and publishing to npm.

    To preview the changes and the release process without actually publishing to npm, use the dry run command.

    # Perform a full release
    npm run release
    
    # Preview the release without publishing
    npm run release-dry-run
  4. Parse and stringify JSON losslessly

    main

    Use parse and stringify from lossless-json to ensure that large numbers and specific numeric formatting are preserved during the serialization/deserialization process. This prevents the precision loss common with native JSON.parse when dealing with long values or extremely large/small exponents.

    import { parse, stringify } from 'lossless-json'
    
    const text = '{"decimal":2.370,"long":9123372036854000123,"big":2.3e+500}'
    
    // LosslessJSON.parse will preserve all numbers and even the formatting:
    const json = parse(text)
    console.log(stringify(json))
    // '{"decimal":2.370,"long":9123372036854000123,"big":2.3e+500}'
  5. Configure parse() options for numbers and duplicate keys

    main

    The options argument for parse() allows you to control how numbers are handled and how duplicate keys in an object are resolved.

    Properties:

    • parseNumber: A custom function (value: string) => unknown. Input is a string; output can be number, bigint, LosslessNumber, or a custom BigNumber. Default is parseLosslessNumber.
    • onDuplicateKey: A callback to handle duplicate keys. The callback receives an object: { key: string, position: number, oldValue: unknown, newValue: unknown }.

    Common onDuplicateKey patterns:

    • Keep first value: onDuplicateKey: ({ oldValue }) => oldValue
    • Keep latest value: onDuplicateKey: ({ newValue }) => newValue
    • Ignore duplicates (keep first): onDuplicateKey: () => {} (returns undefined)
    • Custom error/logging: onDuplicateKey: ({ key, position }) => { throw new Error(Duplicate ${key} at ${position}); }
    import { parse } from 'lossless-json';
    
    const options = {
      parseNumber: (val) => parseNumberAndBigInt(val),
      onDuplicateKey: ({ oldValue }) => oldValue
    };
    
    const data = parse('{"a": 1, "a": 2}', undefined, options);
    // data.a will be 1
  6. Revive ISO 8601 dates during parsing

    main

    You can use the reviveDate helper as a reviver function in parse() to automatically convert ISO 8601 strings into JavaScript Date objects.

    Warning: This is not enabled by default because it might accidentally convert non-date text fields into Date objects.

    import { parse, reviveDate } from 'lossless-json';
    
    const data = parse('["2022-08-25T09:39:19.288Z"]', reviveDate);
    // output: [ new Date('2022-08-25T09:39:19.288Z') ]
  7. Validate safe numbers during parsing

    main

    If you want to parse JSON into regular JavaScript number types but want to ensure no precision is lost, use a custom numberParser combined with the isSafeNumber utility. If a number is found that cannot be safely represented as a native number, you can throw an error.

    import { parse, isSafeNumber } from 'lossless-json'
    
    const options = {
      parseNumber: (value) => {
        if (!isSafeNumber(value)) {
          throw new Error(`Cannot safely convert value '${value}' into a number`)
        }
        return parseFloat(value)
      }
    }
    
    // Success if all values are safe
    let json = parse('[1,2,3]', undefined, options)
    
    // Throws error for unsafe values
    try {
      let json = parse('[1,2e+500,3]', undefined, options)
    } catch (err) {
      console.log(err) // throws Error 'Cannot safely convert value '2e+500' into a number'
    }
  8. Parse JSON with LosslessJSON.parse()

    main

    Use LosslessJSON.parse(text [, reviver [, options]]) to parse a JSON string. Unlike native JSON.parse, this function defaults to parsing all numeric values into LosslessNumber objects to prevent precision loss.

    Parameters:

    • text: The JSON string to parse.
    • reviver (optional): A function (key, value) => unknown to transform values after parsing.
    • options (optional): An object to customize number parsing and duplicate key handling.

    Throws: SyntaxError if the string is not valid JSON.

    import { parse } from 'lossless-json';
    
    const data = parse('{"number": "123.456"}');
    // data.number is a LosslessNumber instance
  9. Integrate with BigNumber libraries (e.g., decimal.js)

    main

    To use lossless-json with external BigNumber libraries, you must define both a parseNumber function for parse() and a custom stringifier for stringify(). The stringifier requires a test function to identify the type and a stringify function to convert it back to a string.

    import { parse, stringify } from 'lossless-json'
    import Decimal from 'decimal.js'
    
    const parseDecimal = (value) => new Decimal(value)
    
    const decimalStringifier = {
      test: (value) => Decimal.isDecimal(value),
      stringify: (value) => value.toString()
    }
    
    const text = '{"value":2.3e500}'
    const json = parse(text, undefined, { parseNumber: parseDecimal })
    
    const output = {
      result: json.value.times(2)
    }
    
    const str = stringify(output, undefined, undefined, [decimalStringifier])
    // '{"result":4.6e500}'
  10. Parse integers as BigInt using a custom numberParser

    main

    You can customize how numbers are parsed by providing a numberParser in the options object. A common pattern is to parse integer values into native JavaScript bigint and other values into regular number types. You can use utility functions like isInteger to implement this logic.

    import { parse, isInteger } from 'lossless-json'
    
    const options = {
      // parse integer values into a bigint, and use a regular number otherwise
      numberParser: (value) => {
        return isInteger(value) ? BigInt(value) : parseFloat(value)
      }
    }
    
    const text = '[123456789123456789123456789, 2.3, 123]'
    const json = parse(text, null, options)
    // output: [123456789123456789123456789n, 2.3, 123n]
  11. Use reviver and replacer for custom serialization

    main

    The library supports reviver (for parse) and replacer (for stringify) arguments, similar to native JSON methods. This allows you to handle custom data types, such as Date objects, by transforming them into unique structures during stringification and reconstructing them during parsing.

    import { parse, stringify } from 'lossless-json'
    
    function customDateReplacer(key, value) {
      if (value instanceof Date) {
        return { $date: value.toISOString() }
      }
      return value
    }
    
    function isJSONDateObject(value) {
      return value && typeof value === 'object' && value.$date === 'string'
    }
    
    function customDateReviver(key, value) {
      if (isJSONDateObject(value)) {
        return new Date(value.$date)
      }
      return value
    }
    
    const record = { message: 'Hello', timestamp: new Date('2022-08-30T09:00:00Z') }
    const text = stringify(record, customDateReplacer)
    const parsed = parse(text, customDateReviver)