# kawanet/msgpack-lite: a pure JavaScript MessagePack codec with a byte cap on its browser build

> msgpack-lite encodes and decodes MessagePack in plain JavaScript, with encode and decode, streaming variants, an extension type table that round trips JavaScript's own types, a codec system for your own classes, and a command line tool that pipes JSON through MessagePack. Its build enforces a hard size limit on the minified browser bundle and smoke tests that bundle in both consumer shapes.

**kawanet/msgpack-lite** — Fast Pure JavaScript MessagePack Encoder and Decoder / msgpack.org[JavaScript]

- Repository: https://github.com/kawanet/msgpack-lite
- Website: https://www.npmjs.com/package/msgpack-lite
- Stars: 1,035 · Forks: 133
- Language: JavaScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/kawanet-msgpack-lite

## Two functions, and the rest is detail

msgpack-lite is a fast pure JavaScript MessagePack encoder and decoder, published on npm as `msgpack-lite` and MIT licensed. The core is small.

```js
const msgpack = require("msgpack-lite");

// encode from JS Object to MessagePack (Buffer)
const buffer = msgpack.encode({"foo": "bar"});

// decode from MessagePack (Buffer) to JS Object
const data = msgpack.decode(buffer); // => {"foo": "bar"}

// if encode/decode receives an invalid argument an error is thrown
```

Two details in that snippet matter. The output of `encode` is a Buffer, so the encoding is binary and not a string, and the error behaviour is explicit, an invalid argument throws rather than producing a silently wrong result.

The decoder is more forgiving about its input than you would expect, which is convenient when the bytes come from somewhere you do not control. `decode()` accepts a Buffer instance by default, and it also accepts an Array instance and a raw Uint8Array instance:

```js
const msgpack = require("msgpack-lite");

// decode() accepts Buffer instance per default
msgpack.decode(Buffer.from([0x81, 0xA3, 0x66, 0x6F, 0x6F, 0xA3, 0x62, 0x61, 0x72]));

// decode() also accepts Array instance
msgpack.decode([0x81, 0xA3, 0x66, 0x6F, 0x6F, 0xA3, 0x62, 0x61, 0x72]);

// decode() accepts raw Uint8Array instance as well
msgpack.decode(new Uint8Array([0x81, 0xA3, 0x66, 0x6F, 0x6F, 0xA3, 0x62, 0x61, 0x72]));
```

The package installs with `npm install --save msgpack-lite`, and there is an online demo at the project's GitHub Pages site for trying the codec in a browser without installing anything.

## Streams, for data that does not arrive all at once

The library provides encoding and decoding streams, and the API is the Node stream API, so it composes with anything else in that style.

```js
const fs = require("fs");
const msgpack = require("msgpack-lite");

const writeStream = fs.createWriteStream("test.msp");
const encodeStream = msgpack.createEncodeStream();
encodeStream.pipe(writeStream);

// send multiple objects to stream
encodeStream.write({foo: "bar"});
encodeStream.write({baz: "qux"});

// call this once you're done writing to the stream.
encodeStream.end();
```

The reading side mirrors it, with the decoded objects arriving as data events:

```js
const fs = require("fs");
const msgpack = require("msgpack-lite");

const readStream = fs.createReadStream("test.msp");
const decodeStream = msgpack.createDecodeStream();

// show multiple objects decoded from stream
readStream.pipe(decodeStream).on("data", console.warn);
```

This is the part of the library that makes it more than a codec, because a MessagePack stream is how the format is usually used in practice, a sequence of self delimiting values rather than one document. The implementation is built on `event-lite`, a small event emitter, which is one of only four runtime dependencies, the others being `ieee754` for float handling, `int64-buffer` for 64 bit integers and `isarray` for array detection.

Four dependencies for a binary codec is a deliberately small surface, and it is the direct consequence of the pure JavaScript approach. There is no native build step, no prebuilt binary to match your platform, and therefore no install time surprise.

## The type mapping, and the undefined to null upgrade

The README publishes a mapping table between JavaScript values and MessagePack formats, and one row deserves attention.

`null` and `undefined` both map to the nil format family, and both decode back as `null`. The note is explicit that both are mapped to the nil `0xC1` type, and that this means an `undefined` value is upgraded to `null` in other words. That is a lossy step, and it is the kind of thing that produces a bug report a year later when a field that was `undefined` on the way in comes back as `null` and a `typeof` check behaves differently.

The rest of the table is unsurprising. A Boolean maps to the bool family, a 32 bit integer Number to the int family, a 64 bit double to the float family, a String to the str family, a Buffer to the bin family, an Array to the array family, a Map to the map family, and a plain object to the map family, decoded as an Object, or as a Map when `usemap=true`.

The `usemap` option is worth knowing about, because it is the difference between getting a Map back and getting a plain object with string keys. If your data contains keys that are not strings, or you rely on insertion order, or you want to distinguish a missing key from a key whose value is null, `usemap` is the option that preserves the distinction.

And the last row is the interesting one, an object in the extension format family, which is how the library round trips values that MessagePack has no native type for.

## Extension types: a table for built ins, a codec for your own

The MessagePack specification allows 128 application specific extension types, and msgpack-lite uses a good number of them so that JavaScript's own types survive a round trip.

The built-in table assigns the low bytes to the standard error types, with `EvalError` at 0x01 through `URIError` at 0x06, and then to the types that JSON cannot represent at all, `RegExp` at 0x0A, `Boolean` at 0x0B, `String` at 0x0C, `Date` at 0x0D, `Error` at 0x0E and `Number` at 0x0F. The typed arrays take the next block, `Int8Array` at 0x11 through `Float64Array` at 0x18, with `Uint8ClampedArray` at 0x19, `ArrayBuffer` at 0x1A, `Buffer` at 0x1B and `DataView` at 0x1D. Extension types that are not in the table are mapped to a built-in ExtBuffer object.

That table is the practical reason to use this library rather than writing a codec, because `Date` and `RegExp` and the typed arrays are exactly the values that a naive MessagePack implementation turns into something lossy.

For your own classes, you register a codec:

```js
const msgpack = require("msgpack-lite");

const codec = msgpack.createCodec();
codec.addExtPacker(0x3F, MyVector, myVectorPacker);
codec.addExtUnpacker(0x3F, myVectorUnpacker);

const data = new MyVector(1, 2);
const encoded = msgpack.encode(data, {codec: codec});
const decoded = msgpack.decode(encoded, {codec: codec});

function MyVector(x, y) {
  this.x = x;
  this.y = y;
}

function myVectorPacker(vector) {
  const array = [vector.x, vector.y];
  return msgpack.encode(array); // return Buffer serialized
}

function myVectorUnpacker(buffer) {
  const array = msgpack.decode(buffer);
  return new MyVector(array[0], array[1]); // return Object deserialized
}
```

The type number must be an integer in the range 0 to 127, and the packer and unpacker are inverses of each other, a packer returning a serialized buffer and an unpacker returning the object. You can also pass an array of functions to `addExtPacker` or `addExtUnpacker`, in which case the value passes through each in order, which allows a compact declaration such as `codec.addExtPacker(0x00, Date, [Number, msgpack.encode])`.

Two details about codec construction catch people. A codec created with `msgpack.createCodec()` and no options does not have the preset extension types defined, and the `preset` option is what gives you the built-in table. And the default codec is reachable at `msgpack.codec.preset` if you want to modify it rather than replace it.

## The CLI, and the browser build with its size budget

There is a command line tool, `bin/msgpack`, which converts a data stream from JSON to MessagePack and vice versa, and the flags are terse. The `-J` means JSON and `-m` means MessagePack, so the pipe reads either way:

```sh
$ echo '{"foo": "bar"}' | ./bin/msgpack -Jm | od -tx1
0000000    81  a3  66  6f  6f  a3  62  61  72

$ echo '{"foo": "bar"}' | ./bin/msgpack -Jm | ./bin/msgpack -Mj
{"foo":"bar"}
```

The `od` output is the interesting part, because it shows the encoded form of a one key map with a three character value, and it is a useful sanity check that the implementation is producing what the specification says.

For the browser there is a minified build, `dist/msgpack.min.js`, and the README states the size as 50KB minified and 14KB gzipped. The usage in a page is a script tag, and the older browser case is handled with conditional comments that load an ES5 shim and json3 for IE9 and below, since the library targets ES5 and older engines need those polyfills for the language features it uses.

The build enforces the size claim rather than asserting it. The Makefile bundles the library with browserify, minifies it with uglify using `arrows=false` and `reserved=[Buffer]`, and then checks the result against a cap:

```
MINJS_MAX_BYTES := 60000
```

and the check fails the build if the file is larger. So the 50KB figure in the README is not a snapshot that can drift, it is a ceiling the build enforces every time.

## Tests that check the bundle, not just the library

The test target in the Makefile is `jshint test-dep smoke-minjs` followed by `node --test` over the test files, and the two extra targets are the interesting part.

`test-dep` runs the check that requiring the library must stay silent, and the comment explains why it is not simply `node index.js`. Deprecation warnings only surface under `--pending-deprecation`, so the target asks for them explicitly, and fails if anything is printed. A library that starts emitting a deprecation warning in a patch release is a maintenance problem for every consumer, and this target catches it before it ships.

`smoke-minjs` then tests the built bundle in both consumer shapes. The comment describes them: a browser `script` tag, where the UMD wrapper leaves a namespace global behind once the CommonJS branch is out of the way, and a CommonJS `require()`, which is how bundlers pull the same file in from a CDN. The target loads the minified file in a fake global environment and checks that each expected export is a function, and separately that the Node only exports are not present in the browser build.

That last distinction is a real API contract. The Makefile names `NAMED_EXPORTS` as `encode decode Encoder Decoder createCodec`, and `NODE_ONLY_EXPORTS` as `createEncodeStream createDecodeStream`. So the browser build deliberately omits the streaming API, and the smoke test fails if it ever appears there by accident, which is the kind of bug that would otherwise only show up in a browser console.

The development dependencies also include other MessagePack implementations, `@msgpack/msgpack`, `msgpack5` and `msgpackr`, plus `notepack.io`, which is the pattern you want in a codec: test your output against other implementations rather than only against yourself.

## Where msgpack-lite is the wrong choice

The first wrong case is a throughput bound service. The package's own pitch is that it is fast for a pure JavaScript implementation, and the development dependencies show that the author benchmarks against `msgpackr` and `msgpack5`, which are the native and faster options. If you are encoding large volumes of data in a hot path, a native codec will do the same work with less CPU, and the portability you are buying is not worth that.

The second is a data model that depends on `undefined` surviving. The nil mapping means an `undefined` value comes back as `null`, and if your code distinguishes the two anywhere downstream, that is a semantic change rather than a serialisation detail.

The third is a protocol where the extension type space is already taken. The library has claimed 0x00 to 0x1F for the built-in JavaScript types, and a custom type has to be a number between 0 and 127. If you are interoperating with a system that has already assigned those bytes, the collision is silent, and the decoder will produce a Date where the other side meant something else.

The fourth is a project that wants a stable API. The package is at version 0.2.2, and the release history is recent and rapid, with 0.2.0 on 2026-08-26, 0.2.1 on 2026-08-27 and 0.2.2 on 2026-09-02, and the last push on 2026-10-09. A 0.x version carries no compatibility promise, so a consumer pinning a version is exposed to breaking changes in a minor release, and the codec is exactly the kind of API where a rename is a breaking change for you.

For what it is, though, the package is well made. The size budget, the silent require check, the two shape smoke test and the cross implementation test dependencies are the marks of a maintainer who treats a codec as something other people's systems depend on.

## Conclusion

Adopt msgpack-lite when you want MessagePack in a browser or a Node service without a native module to compile, and when you need the extension type system to round trip JavaScript's own types, since that table is the part a hand rolled codec usually lacks. Do not adopt it when throughput is the primary constraint, because a pure JavaScript implementation trades the speed of a native codec for portability. Verify first that your data survives the mapping, in particular that `undefined` becomes `null` on the way out, that the extension type number you want for a custom class is free in the 0 to 127 range, and that your runtime meets the development floor of Node 22.18.0 if you plan to work on the package itself.

## FAQ

### How do I install and use msgpack-lite?

Install it with `npm install --save msgpack-lite`, then `require("msgpack-lite")` and use `msgpack.encode(object)` to produce a Buffer and `msgpack.decode(buffer)` to read it back. The decode function also accepts an Array or a raw Uint8Array, and an invalid argument throws an error.

### What happens to undefined values in msgpack-lite?

Both `null` and `undefined` are mapped to the nil `0xC1` type, and both decode back as `null`, so an `undefined` value is upgraded to `null` in other words. That step is lossy, and the README calls it out explicitly.

### How do I serialize my own class with msgpack-lite?

Create a codec, register an extension type number and a packer and unpacker, and pass the codec in the options. The type number must be an integer between 0 and 127, and the packer returns a serialized buffer while the unpacker returns the object.

### Does msgpack-lite work in a browser?

Yes. A minified browser build is published at `dist/msgpack.min.js`, described as 50KB minified and 14KB gzipped, and the build enforces a 60000 byte cap on that file so the size cannot silently grow. The streaming API is Node only and is excluded from the browser build.

### What does the msgpack command line tool do?

The `bin/msgpack` tool converts a data stream from JSON to MessagePack and vice versa, with `-J` for JSON and `-m` for MessagePack, so `echo '{"foo": "bar"}' | ./bin/msgpack -Jm | ./bin/msgpack -Mj` round trips a value through the binary format.

## Sources

- [kawanet/msgpack-lite on GitHub](https://github.com/kawanet/msgpack-lite)
- [License: MIT](https://github.com/kawanet/msgpack-lite/blob/main/LICENSE)
- [Project website](https://www.npmjs.com/package/msgpack-lite)
- [README](https://github.com/kawanet/msgpack-lite/blob/main/README.md)
- [Releases](https://github.com/kawanet/msgpack-lite/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/kawanet-msgpack-lite
