buf.toJSON()

Returns the { type: 'Buffer', data: [...] } object that JSON.stringify uses to serialise a Buffer.

Signature
buf.toJSON()
  • Added in Node.js v0.9.2

Return value

ObjectAn object of the form { type: 'Buffer', data: [...] } where data is an array of the buffer's byte values.

Errors

Does not throw for ordinary buffers, though JSON.stringify on a very large buffer can exceed the maximum string length.

You rarely call buf.toJSON() yourself. JSON.stringify() calls it for you, which is why a Buffer anywhere in a response body turns into {"type":"Buffer","data":[72,105,33]} instead of readable text. This page exists because that output is what people search for.

Why the format looks like that

JSON has no binary type. The type tag marks the object as a serialised Buffer and data lists the byte values, so nothing is lost — but it is verbose: roughly four JSON characters per byte, against 1.37 for base64. For anything larger than a few hundred bytes, encode explicitly with buf.toString('base64').

Getting the Buffer back

JSON.parse() returns a plain object; it does not know to rebuild a Buffer. Pass the data array to Buffer.from(), or supply a reviver that spots the { type: 'Buffer' } shape and converts it. Buffer.isBuffer() on the parsed value returns false, which is the usual way this bug is finally noticed.

The same shape appears in util.inspect output of nested structures and in messages passed through child_process IPC, which uses JSON on the wire.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Understand why an API response contains a type/data object instead of a string.
  • Rebuild a Buffer on the receiving side of a JSON API or IPC channel.
  • Convert a Buffer field to base64 in a toJSON override before serialising.
  • Snapshot a small buffer's exact bytes in a test fixture.

Frequently asked questions

Why does JSON.stringify print { type: 'Buffer', data: [...] } for my Buffer?

Because JSON has no binary type, so Buffer defines a toJSON method that JSON.stringify calls automatically, returning a tagged object with an array of byte values. Call buf.toString('base64') before serialising if you want a compact string instead.

How do I convert { type: 'Buffer', data: [...] } back to a Buffer?

Pass the data array to Buffer.from, as in Buffer.from(obj.data). JSON.parse alone leaves it as a plain object, so Buffer.isBuffer on it returns false.

Should I send Buffers as JSON?

Not as the default toJSON shape, which costs roughly four JSON characters per byte. Encode with buf.toString('base64') for binary inside a JSON document, or send the raw bytes with an appropriate content type when the body is only the binary.

More examples

The API response nobody wanted

res.json runs JSON.stringify, which calls toJSON on the Buffer and produces the type/data object. Encoding to base64 yourself is about three times smaller on the wire and is what a browser client can actually use; sending the raw bytes with the right content type is better still when the body is only the binary.

Example · javascript
const express = require('express');
const app = express();

app.get('/avatar-bad', (req, res) => {
  const image = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
  res.json({ image });
  // {"image":{"type":"Buffer","data":[137,80,78,71]}}
});

app.get('/avatar-good', (req, res) => {
  const image = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
  res.json({ image: image.toString('base64') });
  // {"image":"iVBORw=="}
});

app.get('/avatar-raw', (req, res) => {
  res.type('png').send(Buffer.from([0x89, 0x50, 0x4e, 0x47]));
  // sent as bytes, no JSON involved
});

A reviver that restores every Buffer in a payload

The reviver runs on every value, including nested ones, so one function restores Buffers anywhere in the tree. Note it will also convert any user-supplied object that happens to have this shape, so only apply it to data from a source you control.

Example · javascript
function bufferReviver(key, value) {
  if (value !== null &&
      typeof value === 'object' &&
      value.type === 'Buffer' &&
      Array.isArray(value.data)) {
    return Buffer.from(value.data);
  }
  return value;
}

const wire = JSON.stringify({
  id: 7,
  key: Buffer.from('abc'),
  nested: { salt: Buffer.from([1, 2, 3]) }
});

const obj = JSON.parse(wire, bufferReviver);
console.log(Buffer.isBuffer(obj.key));           // true
console.log(Buffer.isBuffer(obj.nested.salt));   // true
console.log(obj.key.toString());                 // abc

Overriding toJSON on your own type

Defining toJSON on a wrapper class lets you choose the wire format once instead of at every call site. The size figures show why: the default byte-array form costs about five characters per byte for high values, while base64 costs about 1.37.

Example · javascript
class Secret {
  constructor(bytes) { this.bytes = bytes; }
  toJSON() { return { kind: 'secret', b64: this.bytes.toString('base64') }; }
}

const s = new Secret(Buffer.from('hunter2'));
console.log(JSON.stringify(s));
// {"kind":"secret","b64":"aHVudGVyMg=="}

// Size comparison for a 1 KiB buffer:
const kib = Buffer.alloc(1024, 0xff);
console.log(JSON.stringify(kib).length);              // 5147
console.log(JSON.stringify(kib.toString('base64')).length); // 1370

Discussion

  • Be the first to comment on this lesson.