This Jackson 3.x extension library is a component to easily read and write MessagePack encoded data through jackson-databind API.
It extends standard Jackson streaming API (JsonFactory, JsonParser, JsonGenerator), and as such works seamlessly with all the higher level data abstractions (data binding, tree model, and pluggable extensions).
Requirements: Java 17+ and Jackson 3.x. For Jackson 2.x, use org.msgpack:jackson-dataformat-msgpack from msgpack-java instead.
This repository is where jackson-dataformat-msgpack started before it moved into msgpack-java as msgpack-jackson. It was picked up again for Jackson 3, as a rewrite that encodes and decodes MessagePack on Jackson's own buffers instead of through msgpack-core:
- Jackson 3 is a new major version with a new API, and a standalone module can follow its release cadence.
- msgpack-core has its own buffer layer, so every value crossed two buffer layers. Working directly on Jackson's buffers, as Jackson's own CBOR and Smile modules do, removed that overhead (writes +66%, reads +17% on the same POJO, see
jmh/results/) and brought Jackson's read constraints, name canonicalization and buffer pooling to MessagePack.
How it works inside is described in docs/DESIGN.md.
<dependency>
<groupId>org.komamitsu</groupId>
<artifactId>jackson-dataformat-msgpack</artifactId>
<version>3.0.0</version>
</dependency>libraryDependencies += "org.komamitsu" % "jackson-dataformat-msgpack" % "3.0.0"repositories {
mavenCentral()
}
dependencies {
implementation 'org.komamitsu:jackson-dataformat-msgpack:3.0.0'
}All you need is a MessagePackMapper, which is a tools.jackson.databind.ObjectMapper for MessagePack. You can then use it for MessagePack data in the same way as jackson-databind.
// Instantiate ObjectMapper for MessagePack
ObjectMapper objectMapper = new MessagePackMapper();
// Serialize a Java object to byte array
ExamplePojo pojo = new ExamplePojo("komamitsu");
byte[] bytes = objectMapper.writeValueAsBytes(pojo);
// Deserialize the byte array to a Java object
ExamplePojo deserialized = objectMapper.readValue(bytes, ExamplePojo.class);
System.out.println(deserialized.getName()); // => komamitsuWe strongly recommend calling MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString() if you serialize and/or deserialize BigInteger/BigDecimal values. See Serialize and deserialize BigDecimal as str type internally in MessagePack format for details.
ObjectMapper objectMapper = MessagePackMapper.builder().handleBigIntegerAndBigDecimalAsString().build();// Instantiate ObjectMapper for MessagePack
ObjectMapper objectMapper = new MessagePackMapper();
// Serialize a List to byte array
List<Object> list = new ArrayList<>();
list.add("Foo");
list.add("Bar");
list.add(42);
byte[] bytes = objectMapper.writeValueAsBytes(list);
// Deserialize the byte array to a List
List<Object> deserialized = objectMapper.readValue(bytes, new TypeReference<List<Object>>() {});
System.out.println(deserialized); // => [Foo, Bar, 42]// Instantiate ObjectMapper for MessagePack
ObjectMapper objectMapper = new MessagePackMapper();
// Serialize a Map to byte array
Map<String, Object> map = new HashMap<>();
map.put("name", "komamitsu");
map.put("age", 42);
byte[] bytes = objectMapper.writeValueAsBytes(map);
// Deserialize the byte array to a Map
Map<String, Object> deserialized = objectMapper.readValue(bytes, new TypeReference<Map<String, Object>>() {});
System.out.println(deserialized); // => {name=komamitsu, age=42}Java
// Serialize
Map<String, Object> obj = new HashMap<String, Object>();
obj.put("foo", "hello");
obj.put("bar", "world");
byte[] bs = objectMapper.writeValueAsBytes(obj);
// bs => [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111,
// -93, 98, 97, 114, -91, 119, 111, 114, 108, 100]Ruby
require 'msgpack'
# Deserialize
xs = [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111,
-93, 98, 97, 114, -91, 119, 111, 114, 108, 100]
MessagePack.unpack(xs.pack("C*"))
# => {"foo"=>"hello", "bar"=>"world"}
# Serialize
["zero", 1, 2.0, nil].to_msgpack.unpack('C*')
# => [148, 164, 122, 101, 114, 111, 1, 203, 64, 0, 0, 0, 0, 0, 0, 0, 192]Java
// Deserialize
bs = new byte[] {(byte) 148, (byte) 164, 122, 101, 114, 111, 1,
(byte) 203, 64, 0, 0, 0, 0, 0, 0, 0, (byte) 192};
TypeReference<List<Object>> typeReference = new TypeReference<List<Object>>(){};
List<Object> xs = objectMapper.readValue(bs, typeReference);
// xs => [zero, 1, 2.0, null]tools.jackson.databind.ObjectMapper closes an output stream by default after it writes a value. If you want to serialize multiple values in a row without closing an output stream, disable StreamWriteFeature.AUTO_CLOSE_TARGET.
OutputStream out = new FileOutputStream(tempFile);
ObjectMapper objectMapper = MessagePackMapper.builder()
.disable(StreamWriteFeature.AUTO_CLOSE_TARGET)
.build();
objectMapper.writeValue(out, 1);
objectMapper.writeValue(out, "two");
objectMapper.writeValue(out, 3.14);
out.close();The file now holds three MessagePack values back to back. The next section shows how to read them.
tools.jackson.databind.ObjectMapper closes an input stream by default after it reads a value. If you want to deserialize multiple values in a row without closing an input stream, disable StreamReadFeature.AUTO_CLOSE_SOURCE.
// tempFile holds the three values written in the previous section
FileInputStream in = new FileInputStream(tempFile);
ObjectMapper objectMapper = MessagePackMapper.builder()
.disable(StreamReadFeature.AUTO_CLOSE_SOURCE)
.build();
System.out.println(objectMapper.readValue(in, Integer.class)); // => 1
System.out.println(objectMapper.readValue(in, String.class)); // => two
System.out.println(objectMapper.readValue(in, Double.class)); // => 3.14
in.close();In this mode the parser reads exactly the bytes of the value it returns, so the stream is left positioned at the next value.
Old msgpack-java (e.g 0.6.7) doesn't support MessagePack str8 type. When your application needs to communicate with such an old MessagePack library, you can disable the data type like this:
ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory().setStr8FormatSupport(false));
// A string of 32 to 255 bytes is serialized as str16 instead of str8
byte[] resultWithoutStr8Format = objectMapper.writeValueAsBytes(str8LengthString);Map keys are written as strings, the same text Jackson writes for JSON, so every key reads
back through its usual KeyDeserializer. To write Integer, Long, Short and Byte keys
as MessagePack integers instead, enable integer keys:
ObjectMapper objectMapper = new MessagePackMapper(
new MessagePackFactoryBuilder().supportIntegerKeys(true).build());
byte[] bytes = objectMapper.writeValueAsBytes(Collections.singletonMap(42, "Hello")); // {42: "Hello"}
Map<Integer, String> deserialized = objectMapper.readValue(bytes, new TypeReference<Map<Integer, String>>() {});
System.out.println(deserialized); // => {42=Hello}Other key types stay strings either way.
A key of any other type is written as the same string Jackson writes for JSON: the value of
its @JsonKey or @JsonValue accessor, otherwise its toString(). The mapper must be able to
build the type back from that string: through a single-String constructor, a static
valueOf(String) or fromString(String), a @JsonCreator factory taking one String, a
@JsonDeserialize(keyUsing = ...) on the class or on the map property, or a KeyDeserializer
registered on the mapper. Otherwise writing the key fails with an InvalidDefinitionException, because it could
not be read back.
public class UserId {
private final String value;
@JsonCreator
public UserId(String value) { this.value = value; }
@JsonKey
public String value() { return value; }
// equals() and hashCode() on value
}
ObjectMapper objectMapper = new MessagePackMapper();
byte[] bytes = objectMapper.writeValueAsBytes(Collections.singletonMap(new UserId("u-1"), "Alice")); // {"u-1": "Alice"}
Map<UserId, String> deserialized = objectMapper.readValue(bytes, new TypeReference<Map<UserId, String>>() {});For the same reason a map, a collection or an array (other than byte[]) cannot be a key.
Jackson's JSON output writes such keys as their toString() and fails only when reading them;
this library refuses them when writing. A key serializer you register yourself is not checked,
so pair it with a matching KeyDeserializer.
This check, like the integer handling of Short and Byte keys, is part of MessagePackMapper;
a plain ObjectMapper built on MessagePackFactory does not have it.
MessagePackMapper writes a UUID value as its 36-character string, which reads back as the
same UUID. Jackson would otherwise write it as 16 bytes of binary, which this library does
not read back as a UUID.
By default, for backward compatibility, a BigDecimal is written as a MessagePack integer if it has no fractional part, and otherwise as a float64 if that represents it exactly. A value that fits neither (too many digits for a double, or a magnitude beyond 64-bit integers) fails with IllegalArgumentException. So we strongly recommend calling MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString() to internally handle BigDecimal values as String.
ObjectMapper objectMapper = MessagePackMapper.builder().handleBigIntegerAndBigDecimalAsString().build();
Pojo obj = new Pojo();
// This value is too large to be serialized as double
obj.value = new BigDecimal("1234567890.98765432100");
byte[] converted = objectMapper.writeValueAsBytes(obj);
System.out.println(objectMapper.readValue(converted, Pojo.class)); // => Pojo{value=1234567890.98765432100}MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString() is equivalent to the following configuration.
ObjectMapper objectMapper = MessagePackMapper.builder()
.withConfigOverride(BigInteger.class,
o -> o.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)))
.withConfigOverride(BigDecimal.class,
o -> o.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)))
.build();timestamp extension type is defined in MessagePack as type:-1. Registering TimestampExtensionModule.INSTANCE module enables automatic serialization and deserialization of java.time.Instant to/from the MessagePack extension type.
ObjectMapper objectMapper = MessagePackMapper.builder()
.addModule(TimestampExtensionModule.INSTANCE)
.build();
Pojo pojo = new Pojo();
// The type of `timestamp` variable is Instant
pojo.timestamp = Instant.now();
byte[] bytes = objectMapper.writeValueAsBytes(pojo);
// The Instant instance is serialized as MessagePack extension type (type: -1)
Pojo deserialized = objectMapper.readValue(bytes, Pojo.class);
System.out.println(deserialized); // "2022-09-14T08:47:24.922Z"ExtensionTypeCustomDeserializers helps you to deserialize your own custom extension types easily.
// In this application, extension type 59 is used for byte[]
byte[] bytes = new MessagePackMapper().writeValueAsBytes(new MessagePackExtensionType((byte) 59, hexspeak));
// Register the type and a deserializer to ExtensionTypeCustomDeserializers
ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers();
extTypeCustomDesers.addCustomDeser((byte) 59, data -> {
if (Arrays.equals(data,
new byte[] {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE})) {
return "Java";
}
return "Not Java";
});
ObjectMapper objectMapper = new MessagePackMapper(
new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers));
System.out.println(objectMapper.readValue(bytes, Object.class));
// => Javastatic class TripleBytesPojo
{
public byte first;
public byte second;
public byte third;
public TripleBytesPojo(byte first, byte second, byte third)
{
this.first = first;
this.second = second;
this.third = third;
}
@Override
public boolean equals(Object o)
{
:
}
@Override
public int hashCode()
{
:
}
@Override
public String toString()
{
// This key format is used when serialized as map key
return String.format("%d-%d-%d", first, second, third);
}
static class KeyDeserializer
extends tools.jackson.databind.KeyDeserializer
{
@Override
public Object deserializeKey(String key, DeserializationContext ctxt)
{
String[] values = key.split("-");
return new TripleBytesPojo(Byte.parseByte(values[0]), Byte.parseByte(values[1]), Byte.parseByte(values[2]));
}
}
static TripleBytesPojo deserialize(byte[] bytes)
{
return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]);
}
}
:
byte extTypeCode = 42;
ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers();
extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser()
{
@Override
public Object deserialize(byte[] value)
throws IOException
{
return TripleBytesPojo.deserialize(value);
}
});
SimpleModule module = new SimpleModule();
module.addKeyDeserializer(TripleBytesPojo.class, new TripleBytesPojo.KeyDeserializer());
ObjectMapper objectMapper = MessagePackMapper.builder(
new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers))
.addModule(module)
.build();
Map<TripleBytesPojo, Integer> deserializedMap =
objectMapper.readValue(serializedData,
new TypeReference<Map<TripleBytesPojo, Integer>>() {});static class TripleBytesPojo
{
public byte first;
public byte second;
public byte third;
public TripleBytesPojo(byte first, byte second, byte third)
{
this.first = first;
this.second = second;
this.third = third;
}
static class Deserializer
extends StdDeserializer<TripleBytesPojo>
{
protected Deserializer()
{
super(TripleBytesPojo.class);
}
@Override
public TripleBytesPojo deserialize(JsonParser p, DeserializationContext ctxt)
{
return TripleBytesPojo.deserialize(p.getBinaryValue());
}
}
static TripleBytesPojo deserialize(byte[] bytes)
{
return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]);
}
}
:
byte extTypeCode = 42;
ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers();
extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser()
{
@Override
public Object deserialize(byte[] value)
throws IOException
{
return TripleBytesPojo.deserialize(value);
}
});
SimpleModule module = new SimpleModule();
module.addDeserializer(TripleBytesPojo.class, new TripleBytesPojo.Deserializer());
ObjectMapper objectMapper = MessagePackMapper.builder(
new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers))
.addModule(module)
.build();
Map<String, TripleBytesPojo> deserializedMap =
objectMapper.readValue(serializedData,
new TypeReference<Map<String, TripleBytesPojo>>() {});