Skip to content

Instantly share code, notes, and snippets.

@jimmywarting
Last active August 15, 2022 10:25
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jimmywarting/a6ae45a9f445ca352ed62374a2855ff2 to your computer and use it in GitHub Desktop.
Save jimmywarting/a6ae45a9f445ca352ed62374a2855ff2 to your computer and use it in GitHub Desktop.
Custom json replacer/reviver with extended support

This custom json replacer/reviver allow you to store more types as json and still keep it as valid json.

features

  • Allow you to stringify/parse
    • BigInt
    • Infinity
    • NaN
    • Date
    • typed arrays & ArrayBuffers as web-safe base64
    • Maps & Sets
    • Error & TypeError

example

var obj = {
  bigint: 2n,
  date: new Date(),
  pInf: Infinity,
  arr: [2,3],
  nInf: -Infinity,
  map: new Map([['a', 'b']]),
  set: new Set(['a', new Date(), 'a']),
  reg: /^f/g,
  err: new Error('fail'),
  typo: new TypeError('typo'),
  empty: null,
  binary: new Uint16Array([97]),
  buff: new ArrayBuffer(20),
  obj: {bday: new Date('1996-08-29')},
  str: 'str',
  bool: true,
  num: 3,
  nan: NaN,
  invDate: new Date('!'),
  // Experimental... creating blob/files are sync but reading it is async.
  file: {$f: [[new Uint8Array([97])], 'sample.txt', {type: 'text/javascript'}]},
  blob: {$d: [['abc'], {type: 'text/javascript'}]}
}

var str = JSON.stringify(obj, replacer, 2)
var res = JSON.parse(str, reviver)

console.log(str)
console.log(obj)
console.log(res) // pretty much the same as obj

This is simular to bson (ejson) but with a much smaller footprint and different data types

const {replacer,reviver} = ((types, b64) => ({
replacer(key) {
var val = this[key]
return val === Infinity ? { $n: 1 }:
val === -Infinity ? { $n: -1 }:
Number.isNaN(val) ? { $n: ' ' }:
val instanceof Date ? { $t: isNaN(val) ? '!' : +val }:
val instanceof Map ? { $m: [...val] }:
val instanceof Set ? { $s: [...val] }:
val instanceof TypeError ? { $1: [val.message, val.stack] }:
val instanceof Error ? { $e: [val.message, val.stack] }:
val instanceof RegExp ? { $r: [val.source, val.flags] }:
ArrayBuffer.isView(val) || val instanceof ArrayBuffer ? { $b: [types.indexOf(val.constructor), b64.encode(new Uint8Array(val))]}:
typeof val === 'bigint' ? { $i: val+'' }:
val
},
reviver: (key, val) =>
val === null && val !== 'object' ? val:
val.$n ? val.$n/0:
val.$t ? new Date(val.$t):
val.$r ? new RegExp(...val.$r):
val.$f ? new File(...val.$f):
val.$d ? new Blob(...val.$d):
val.$e ? (key = new Error(val.$e[0]), key.stack = val.$e[1], key):
val.$1 ? (key = new TypeError(val.$1[0]), key.stack = val.$1[1], key):
val.$m ? new Map(val.$m):
val.$s ? new Set(val.$s):
val.$b ? val.$b[0]
? new types[val.$b[0]](b64.decode(val.$b[1]))
: new Uint8Array(b64.decode(val.$b[1])).buffer:
val.$i ? BigInt(val.$i):
val
}))([ArrayBuffer, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, BigInt64Array, BigUint64Array], (()=>{var f=[65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,48,49,50,51,52,53,54,55,56,57,45,95,61],h=[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,62,null,62,null,63,52,53,54,55,56,57,58,59,60,
61,null,null,null,64,null,null,null,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,null,null,null,null,63,null,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,null,null];return{decode(a){var b=a.length%4;b&&(a+=Array(5-b).join("="));b=-1;var f=new ArrayBuffer(a.length/4*3),d,e=new Uint8Array(f),c=0;for(d=a.length;++b<d;){var g=h[a.charCodeAt(b)],k=h[a.charCodeAt(++b)];e[c++]=g<<2|k>>4;g=h[a.charCodeAt(++b)];if(64===g)break;e[c++]=(k&15)<<
4|g>>2;k=h[a.charCodeAt(++b)];if(64===k)break;e[c++]=(g&3)<<6|k}return new Uint8Array(f,0,c)},encode(a){for(var b=-1,h=a.length,d=new Uint8Array(new ArrayBuffer(Math.ceil(4*h/3))),e=0;++b<h;){var c=a[b],g=a[++b];d[e++]=f[c>>2];d[e++]=f[(c&3)<<4|g>>4];isNaN(g)?(d[e++]=f[64],d[e++]=f[64]):(c=a[++b],d[e++]=f[(g&15)<<2|c>>6],d[e++]=f[isNaN(c)?64:c&63])}return new TextDecoder().decode(d)}}})())
@jimmywarting
Copy link
Author

suggestions/ideas are welcome.
You are free to use this however you like in your own project.

@monsterbitar
Copy link

I had some issues trying to find something like this and since I had experience with using the bufferReviver from MDN examples, I kept looking in the wrong direction (need: reviver). Since buffers and arraybuffers encode differently by default, using both a replacer and reviver seemed much more sensible and this gist helped me see the issue more clearly.

The only part of this gist I felt was off, was that it tries to JSON encode but uses data structure that are minimal and unintuitive - making the data significantly harder to work with for a human should they inspect it in encoded form.

As a result, I decided to build something similar, inspired by this, but with a smaller scope: https://gitlab.com/monsterbitar/json-arraybuffer-reviver

Thank you for the inspiration <3

@connorjclark
Copy link

connorjclark commented May 9, 2021

If you change res.binary to new Uint16Array([1000]) you'll see that the values are wrapped into 8-bit slots.

ArrayBuffer.isView(val) || val instanceof ArrayBuffer ? { $b: [types.indexOf(val.constructor), b64.encode(new Uint8Array(val))]}:

The issue is that new Uint8Array(val) truncates values to fit. I made these changes to fix:

  1. pass the typedarray.buffer to encode and read it back from decode
  2. pass BYTES_PER_ELEMENT to decode to align the ArrayBuffer to the correct number of bytes (others the ctors error)

The second change technically makes typed arrays that have a couple bytes too long after a JSON roundtrip if the number of bytes isn't perfectly aligned with the 4/3 byte ratio that the encoding uses. It's not noticeable programmatic with normal usage of TypeArray interface, but if you grab the underlying buffer you may be surprised to find an extra 1 or 2 nil bytes at the end. I couldn't figure out how to resolve this.


Full Code
const {replacer,reviver} = ((types, b64) => ({
  // @ts-expect-error
  replacer(key) {
    // @ts-expect-error
    var val = this[key]

    return val === Infinity ? { $n: 1 }:
           val === -Infinity ? { $n: -1 }:
           Number.isNaN(val) ? { $n: ' ' }:
           // @ts-expect-error
           val instanceof Date ? { $t: isNaN(val) ? '!' : +val }:
           val instanceof Map ? { $m: [...val] }:
           val instanceof Set ? { $s: [...val] }:
           val instanceof TypeError ? { $1: [val.message, val.stack] }:
           val instanceof Error ? { $e: [val.message, val.stack] }:
           val instanceof RegExp ? { $r: [val.source, val.flags] }:
           // @ts-expect-error
           ArrayBuffer.isView(val) || val instanceof ArrayBuffer ? { $b: [types.indexOf(val.constructor), b64.encode(new Uint8Array(val.buffer))]}:
           typeof val === 'bigint' ? { $i: val+'' }:
           val
  },
  // @ts-expect-error
  reviver: (key, val) =>
      val === null && val !== 'object' ? val:
      val.$n ? val.$n/0:
      val.$t ? new Date(val.$t):
      // @ts-expect-error
      val.$r ? new RegExp(...val.$r):
      // @ts-expect-error
      val.$f ? new File(...val.$f):
      val.$d ? new Blob(...val.$d):
      val.$e ? (key = new Error(val.$e[0]), key.stack = val.$e[1], key):
      val.$1 ? (key = new TypeError(val.$1[0]), key.stack = val.$1[1], key):
      val.$m ? new Map(val.$m):
      val.$s ? new Set(val.$s):
      val.$b ? val.$b[0]
        // @ts-expect-error
        ? new types[val.$b[0]](b64.decode(val.$b[1], types[val.$b[0]].BYTES_PER_ELEMENT).buffer)
        : new Uint8Array(b64.decode(val.$b[1], 1)).buffer:
      val.$i ? BigInt(val.$i):
      val 
}))([ArrayBuffer, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, BigInt64Array, BigUint64Array], (()=>{var f=[65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,48,49,50,51,52,53,54,55,56,57,45,95,61],h=[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,62,null,62,null,63,52,53,54,55,56,57,58,59,60,
// @ts-expect-error
61,null,null,null,64,null,null,null,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,null,null,null,null,63,null,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,null,null];return{decode(a,bytesper){ var b=a.length%4;b&&(a+=Array(5-b).join("="));b=-1;var l=a.length/4*3; l = l - l%bytesper; var f=new ArrayBuffer(l),d,e=new Uint8Array(f),c=0;for(d=a.length;++b<d;){var g=h[a.charCodeAt(b)],k=h[a.charCodeAt(++b)];e[c++]=g<<2|k>>4;g=h[a.charCodeAt(++b)];if(64===g)break;e[c++]=(k&15)<<
// @ts-expect-error
4|g>>2;k=h[a.charCodeAt(++b)];if(64===k)break;e[c++]=(g&3)<<6|k} return new Uint8Array(f,0,c)},encode(a){for(var b=-1,h=a.length,d=new Uint8Array(new ArrayBuffer(Math.ceil(4*h/3))),e=0;++b<h;){var c=a[b],g=a[++b];d[e++]=f[c>>2];d[e++]=f[(c&3)<<4|g>>4];isNaN(g)?(d[e++]=f[64],d[e++]=f[64]):(c=a[++b],d[e++]=f[(g&15)<<2|c>>6],d[e++]=f[isNaN(c)?64:c&63])}return new TextDecoder().decode(d)}}})())
/* eslint-enable */

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment