Skip to main content
Version: 6.0-SNAPSHOT

Compact Serialization

As an enhancement to existing serialization methods, Hazelcast offers Compact serialization, with the following main features:

  • Separates the schema from the data and stores it per type, not per object which results in less memory and bandwidth usage compared to other formats
  • Does not require a class to implement an interface or change the source code of the class in any way
  • Supports schema evolution which permits adding or removing fields, or changing the types of fields
  • Can work with no configuration or any kind of factory/serializer registration for Java classes, Java records, and .NET classes
  • Platform and language independent
  • Supports partial deserialization of fields during queries or indexing

Hazelcast achieves these features by having well-known schemas of objects and replicating them across the cluster which enables members and clients to fetch schemas they don't have in their local registries. Each serialized object carries just a schema identifier and relies on the schema distribution service or configuration to match identifiers with the actual schema. Once the schemas are fetched, they are cached locally on the members and clients so that the next operations that use the schema do not incur extra costs.

Schemas help Hazelcast to identify the locations of the fields on the serialized binary data. With this information, Hazelcast can deserialize individual fields of the data, without deserializing the whole binary. This results in a better query and indexing performance.

Schemas can evolve freely by adding or removing fields. Even, the types of the fields can be changed. Multiple versions of the schema may live in the same cluster and both the old and new readers may read the compatible parts of the data. This feature is especially useful in rolling upgrade scenarios.

The Compact serialization does not require any changes in the user classes as it doesn't need a class to implement a particular interface. Serializers might be implemented and registered separately from the classes.

It also supports zero-configuration use cases by automatically extracting schemas out of the Java classes and records, and .NET classes which are cached and reused later, with no extra cost.

The underlying format of the Compact serialized objects is platform and language independent.

Configuration​

The configuration can be used to register either

  • an explicit CompactSerializer
  • a zero-config serializer for a class to override other serialization mechanisms, for languages that provide zero-config Compact serialization.

In case of an explicit serializer, you have to supply a unique type name for the class in the serializer.

Choosing a type name will associate that name with the schema and will make the polyglot use cases, where there are multiple clients from different languages, possible. Serializers in different languages can work on the same data, provided that their read and write methods are compatible, and they have the same type name.

note

If you evolve your class in the later versions of your application, by adding or removing fields, you should continue using the same type name for that class.

To register an explicit serializer for a certain class, add the following configuration:

Programmatic Configuration:

java
Config config = new Config();
config.getSerializationConfig()
.getCompactSerializationConfig()
.addSerializer(new FooSerializer());

Declarative Configuration:

xml
<hazelcast>
<serialization>
<compact-serialization>
<serializers>
<serializer>
com.example.FooSerializer
</serializer>
</serializers>
</compact-serialization>
</serialization>
</hazelcast>

Lastly, the following is a sample configuration that registers zero-config serializer for a certain class, without implementing an explicit serializer.

This way, one can override other the serializer of a certain class such as Java Serializable serializer with the zero-config serializer.

When a class is serialized using the zero-config Compact serializer, Hazelcast will choose the fully qualified class name for Java as the type name automatically.

Programmatic Configuration:

java
Config config = new Config();
config.getSerializationConfig()
.getCompactSerializationConfig()
.addClass(Bar.class);

Declarative Configuration:

xml
<hazelcast>
<serialization>
<compact-serialization>
<classes>
<class>
com.example.Bar
</class>
</classes>
</compact-serialization>
</serialization>
</hazelcast>

If you want to override the serialization mechanism used for Serializable or Externalizable classes and use Compact serialization without writing any serializer in Java, you must add those classes to the configuration.

Implementing CompactSerializer​

Compact serialization can be used by implementing a CompactSerializer for a class and registering it in the configuration.

For example, assume that you have the following Employee class.

java
public class Employee {
private long id;
private String name;

public Employee(long id, String name) {
this.id = id;
this.name = name;
}

public long getId() {
return id;
}

public String getName() {
return name;
}
}

Then, a Compact serializer can be implemented as below.

java
public class EmployeeSerializer implements CompactSerializer<Employee> {
@Override
public Employee read(CompactReader reader) {
long id = reader.readInt64("id");
String name = reader.readString("name");
return new Employee(id, name);
}

@Override
public void write(CompactWriter writer, Employee employee) {
writer.writeInt64("id", employee.getId());
writer.writeString("name", employee.getName());
}

@Override
public Class<Employee> getCompactClass() {
return Employee.class;
}

@Override
public String getTypeName() {
return "employee";
}
}

The last step is to register the serializer in the member or client configuration, as shown in the Configuration section.

Upon serialization, a schema will be created from the serializer, and a unique schema identifier will be assigned to it automatically.

After the configuration registration, Hazelcast will serialize instances of the Employee class using the EmployeeSerializer.

warning

Hazelcast uses the write method of the CompactSerializer to generate a schema for Compact serializable classes. When you implement the write method for this purpose, this implementation should be able to generate the same schema regardless of the parameters you pass; it should not contain any conditional code. This means, your write implementation should be repeatable, such that it should call the same methods of CompactWriter no matter the provided instance. Failing to follow this rule might result in an undefined behavior.

Supported Types​

Compact serialization supports the following list as first class types. Any other type can be implemented on top of these, by using these types as building blocks.

TypeJava.NETC++Node.jsPythonGoDescription
BOOLEANbooleanboolboolbooleanboolboolTrue or false represented by a single bit as either 1 or 0. Up to eight booleans are packed into a single byte.
ARRAY_OF_BOOLEANboolean[]bool[]boost::optional<std::vector<bool>>boolean[] | nullOptional[list[bool]][]boolArray of booleans or null. Up to eight boolean array items are packed into a single byte.
NULLABLE_BOOLEANBooleanbool?boost::optional<bool>boolean | nullOptional[bool]*boolA boolean that can also be null.
ARRAY_OF_NULLABLE_BOOLEANBoolean[]bool?[]boost::optional<std::vector<boost::optional<bool>>>(boolean | null)[] | nullOptional[list[Optional[bool]]][]*boolArray of nullable booleans or null.
INT8bytesbyteint8_tnumberintint88-bit two's complement signed integer.
ARRAY_OF_INT8byte[]sbyte[]boost::optional<std::vector<int8_t>>Buffer | nullOptional[list[int]][]int8Array of int8s or null.
NULLABLE_INT8Bytesbyte?boost::optional<int8_t>number | nullOptional[int]*int8An int8 that can also be null.
ARRAY_OF_NULLABLE_INT8Byte[]sbyte?[]boost::optional<std::vector<boost::optional<int8_t>>>(number | null)[] | nullOptional[list[Optional[int]]][]*int8Array of nullable int8s or null.
INT16shortshortint16_tnumberintint1616-bit two's complement signed integer.
ARRAY_OF_INT16short[]short[]boost::optional<std::vector<int16_t>>number[] | nullOptional[list[int]][]int16Array of int16s or null.
NULLABLE_INT16Shortshort?boost::optional<int16_t>number | nullOptional[int]*int16An int16 that can also be null.
ARRAY_OF_NULLABLE_INT16Short[]short?[]boost::optional<std::vector<boost::optional<int16_t>>>(number | null)[] | nullOptional[list[Optional[int]]][]*int16Array of nullable int16s or null.
INT32intintint32_tnumberintint3232-bit two's complement signed integer.
ARRAY_OF_INT32int[]int[]boost::optional<std::vector<int32_t>>number[] | nullOptional[list[int]][]int32Array of int32s or null.
NULLABLE_INT32Integerint?boost::optional<int32_t>number | nullOptional[int]*int32An int32 that can also be null.
ARRAY_OF_NULLABLE_INT32Integer[]int?[]boost::optional<std::vector<boost::optional<int32_t>>>(number | null)[] | nullOptional[list[Optional[int]]][]*int32Array of nullable int32s or null.
INT64longlongint64_tLongintint6464-bit two's complement signed integer.
ARRAY_OF_INT64long[]long[]boost::optional<std::vector<int64_t>>Long[] | nullOptional[list[int]][]int64Array of int64s or null.
NULLABLE_INT64Longlong?boost::optional<int64_t>Long | nullOptional[int]*int64An int64 that can also be null.
ARRAY_OF_NULLABLE_INT64Long[]long?[]boost::optional<std::vector<boost::optional<int64_t>>>(Long | null)[] | nullOptional[list[Optional[int]]][]*int64Array of nullable int64s or null.
FLOAT32floatfloatfloatnumberfloatfloat3232-bit IEEE 754 floating point number.
ARRAY_OF_FLOAT32float[]float[]boost::optional<std::vector<float>>number[] | nullOptional[list[float]][]float32Array of float32s or null.
NULLABLE_FLOAT32Floatfloat?boost::optional<float>number | nullOptional[float]*float32A float32 that can also be null.
ARRAY_OF_NULLABLE_FLOAT32Float[]float?[]boost::optional<std::vector<boost::optional<float>>>(number | null)[] | nullOptional[list[Optional[float]]][]*float32Array of nullable float32s or null.
FLOAT64doubledoubledoublenumberfloatfloat6464-bit IEEE 754 floating point number.
ARRAY_OF_FLOAT64double[]double[]boost::optional<std::vector<double>>number[] | nullOptional[list[float]][]float64Array of float64s or null.
NULLABLE_FLOAT64Doubledouble?boost::optional<double>number | nullOptional[float]*float64A float64 that can also be null.
ARRAY_OF_NULLABLE_FLOAT64Double[]double?[]boost::optional<std::vector<boost::optional<double>>>(number | null)[] | nullOptional[list[Optional[float]]][]*float64Array of nullable float64s or null.
STRINGStringstringstd::stringstring | nullOptional[str]*stringA UTF-8 encoded string or null.
ARRAY_OF_STRINGString[]string[]boost::optional<std::vector<std::string>>(string | null)[] | nullOptional[list[Optional[str]]][]*stringArray of strings or null.
DECIMALBigDecimalHBigDecimalboost::optional<hazelcast::client::big_decimal>BigDecimal | nullOptional[decimal.Decimal]*types.DecimalArbitrary precision and scale floating point number or null.
ARRAY_OF_DECIMALBigDecimal[]HBigDecimal[]boost::optional<std::vector<boost::optional<hazelcast::client::big_decimal>>>(BigDecimal | null)[] | nullOptional[list[decimal.Decimal]][]*types.DecimalArray of decimals or null.
TIMELocalTimeHLocalTimeboost::optional<hazelcast::client::local_time>LocalTime | nullOptional[datetime.time]*types.LocalTimeTime consisting of hours, minutes, seconds, and nanoseconds or null.
ARRAY_OF_TIMELocalTime[]HLocalTime[]boost::optional<std::vector<boost::optional<local_time>>>(LocalTime | null)[] | nullOptional[list[Optional[datetime.time]]][]*types.LocalTimeArray of times or null.
DATELocalDateHLocalDateboost::optional<hazelcast::client::local_date>LocalDate | nullOptional[datetime.date]*types.LocalDateDate consisting of year, month, and day of the month or null.
ARRAY_OF_DATELocalDate[]HLocalDate[]boost::optional<std::vector<boost::optional<local_date>>>(LocalDate | null)[] | nullOptional[list[Optional[datetime.date]]][]*types.LocalDateArray of dates or null.
TIMESTAMPLocalDateTimeHLocalDateTimeboost::optional<hazelcast::client::local_date_time>LocalDateTime | nullOptional[datetime.datetime]*types.LocalDateTimeTimestamp consisting of year, month, day of the month, hour, minutes, seconds, and nanoseconds or null.
ARRAY_OF_TIMESTAMPLocalDateTime[]HLocalDateTime[]boost::optional<std::vector<boost::optional<hazelcast::client::local_date_time>>>(LocalDateTime | null)[] | nullOptional[list[Optional[datetime.datetime]]][]*types.LocalDateTimeArray of timestamps or null.
TIMESTAMP_WITH_TIMEZONEOffsetDateTimeHOffsetDateTimeboost::optional<hazelcast::client::offset_date_time>OffsetDateTime | nullOptional[datetime.datetime]*types.OffsetDateTimeTimestamp with timezone consisting of year, month, day of the month, hour, minutes, seconds, nanoseconds, and offset seconds or null.
ARRAY_OF_TIMESTAMP_WITH_TIMEZONEOffsetDateTime[]HOffsetDateTime[]boost::optional<std::vector<boost::optional<hazelcast::client::offset_date_time>>>(OffsetDateTime | null)[] | nullOptional[list[Optional[datetime.datetime]]][]*types.OffsetDateTimeArray of timestamp with timezones or null.
COMPACTCan be any user type.Can be any user type.Can be any user type.Can be any user type.Can be any user type.anyA user defined nested Compact serializable object or null.
ARRAY_OF_COMPACTCan be an array of any user type.Can be an array of any user type.Can be an array of any user type.Can be an array of any user type.Can be an array of any user type.[]anyArray of user defined Compact serializable objects or null.
note

Compact serialization supports circularly-dependent types, provided that the cycle ends at some point on runtime by some null value.

Using Compact Serialization With Zero-Configuration​

note

The ability to use Compact serialization with no configuration is only available in Java and .NET.

note

Using zero-config Compact serialization is not recommended for performance-critical applications, as the feature relies heavily on reflection to read and write data.
We recommend you use explicit Compact serializers for performance considerations.

Compact serialization can also be used without registering a serializer in the member or client configuration.

When Hazelcast cannot associate a class with any other serialization mechanism, instead of throwing an exception directly, it tries to use zero-configuration Compact serialization as a last effort.

Hazelcast tries to extract a schema out of the class. If successful, it registers the zero-config serializer associated with the extracted schema and uses it while serializing and deserializing instances of that class. If the automatic schema extraction fails, Hazelcast throws an exception.

For example, assume that you have the same Employee class.

If you don't perform any kind of configuration change and use the instances of the class directly, no exceptions will be thrown. Hazelcast will generate a schema out of the Employee class the first time you try to serialize an object, cache it, and reuse it for the subsequent serializations and deserializations.

The same holds true for the Java records. Hazelcast supports serializing and deserializing Java records, without an extra configuration as well.

Assuming the Employee class was a Java record:

java
public record Employee(long id, String name) {
}

The following code would work for both of them.

java
HazelcastInstance client = HazelcastClient.newHazelcastClient();
IMap<Long, Employee> map = client.getMap("employees");
Employee employee = new Employee(1L, "John Doe");
map.set(1L, employee);
Employee employeeFromMap = map.get(1L);

Currently, Hazelcast supports extracting schemas out of classes that have the field types shown above and some others, for user convenience.

For Java, the zero-config serializer supports the following extra field types on top of the first class types:

  • char, represented as an INT16
  • Character, represented as a NULLABLE_INT16
  • Enum, represented as a STRING, using the names of the enum members.
  • Arrays of the types listed above, represented by their respective arrays. *
  • List or ArrayList of the types listed above and first class types, represented by their respective arrays with the same field name. Fields of type List are deserialized as ArrayList upon reads. *
  • Set or HashSet of the types listed above and first class types, represented by their respective arrays with the same field name. Fields of type Set are deserialized as HashSet upon reads. *
  • Map or HashMap or the types listed above and first class types, represented by two arrays, one for keys and one for values, represented by their respective arrays for key and value types. The names of those arrays are of the form fieldName + '!keys' and fieldName + '!values'. Fields of type Map are deserialized as HashMap upon reads. *
note

* Arrays of arrays, or collections of collections are not supported by default. An explicit serializer must be written to support such field types. In that serializer, the inner array type must be defined as a separate class which stores the array type as a field. Then, the array of array type can be serialized/deserialized as an array of that separate class.

For Java APIs, a zero-config Compact serializer uses reflection to read and write to fields of objects, regardless of whether those fields are public. You must enable reflective access for packages of your module by using the opens statement in the module declaration:

module org.example.Foo {
opens org.example to com.hazelcast.core;
}

Restricting Classes for Zero-Config Compact Serialization​

Zero-config Compact serialization uses reflection to serialize any class that does not have another registered serializer. To control which classes are eligible for this, and to prevent classes from being serialized unintentionally, configure a filter that allows or blocks classes.

This filter applies only to zero-config Compact serialization. Classes that have an explicitly registered serializer are not affected by the filter, even if the filter would otherwise block them.

The filter uses an allowlist and a blocklist, in the same format as the Java serialization filter. You can allow or block classes by fully qualified class name, package name, or class-name prefix. The filter can be set on both members and clients, and the configuration does not have to be the same on each.

When a filter is configured:

  • If a class is on the blocklist, serializing an object of that class throws an exception.
  • If a class is on the blocklist, deserializing its data returns a GenericRecord instead of an instance of the class, rather than throwing an exception. This means member-side code, such as an entry processor, can still access the data through the GenericRecord, and features that read individual fields, such as the Predicate API and SQL, can still process the data without deserializing it into an instance of the blocked class.
  • If the allowlist is non-empty, only classes on the allowlist which are not on the blocklist are eligible for zero-config Compact serialization.

Without a configured filter, Hazelcast applies a built-in blocklist of internal classes. This does not cover your own classes, so for the strongest protection, we suggest defining an allowlist containing only the classes your application needs.

To disable zero-config Compact serialization entirely, configure an empty allowlist and set defaults-disabled to true.

xml
<hazelcast>
...
<serialization>
<compact-serialization>
<zero-config-filter defaults-disabled="false">
<whitelist>
<class>example.Foo</class>
<package>com.acme.app</package>
<prefix>com.acme.</prefix>
</whitelist>
<blacklist>
<class>com.acme.app.BeanComparator</class>
</blacklist>
</zero-config-filter>
</compact-serialization>
</serialization>
...
</hazelcast>

Schema Evolution​

This section provides an introduction to schema evolution, explaining how Compact serialization allows your object definitions to change safely over time. A more comprehensive guide covering all schema change scenarios is available in the Schema Evolution page.

Compact serialization permits schemas and classes to evolve by adding or removing fields, or by changing the types of fields. More than one version of a class may live in the same cluster and different clients or members might use different versions of the class.

Hazelcast handles the versioning internally. So, you don't have to change anything in the classes or serializers apart from the added, removed, or changed fields.

Hazelcast achieves this by identifying each version of the class by a unique fingerprint. Any change in a class results in a different fingerprint. Hazelcast uses a 64-bit Rabin Fingerprint to assign identifiers to schemas, which has an extremely low collision rate.

Different versions of the schema with different identifiers are replicated in the cluster and can be fetched by clients or members internally. That allows old readers to read fields of the classes they know when they try to read data serialized by a new writer. Similarly, new readers might read fields of the classes available in the data, when they try to read data serialized by an old writer.

Assume that the two versions of the following Employee class lives in the cluster.

java
public class Employee {
private long id;
private String name;
}
java
public class Employee {
private long id;
private String name;
private int age; // Newly added field
}

Then, when faced with binary data serialized by the new writer, old readers will be able to read the following fields.

java
public class EmployeeSerializer implements CompactSerializer<Employee> {
@Override
public Employee read(CompactReader reader) {
long id = reader.readInt64("id");
String name = reader.readString("name");
// The new "age" field is there, but the old reader does not
// know anything about it. Hence, it will simply ignore that field.
return new Employee(id, name);
}
...
}

Then, when faced with binary data serialized by the old writer, new readers will be able to read the following fields. Also, Hazelcast provides convenient APIs to check the existence of fields in the data when there is no such field.

java
public class EmployeeSerializer implements CompactSerializer<Employee> {
@Override
public Employee read(CompactReader reader) {
long id = reader.readInt64("id");
String name = reader.readString("name");
// Read the "age" if it exists, or use the default value 0.
// reader.readInt32("age") would throw if the "age" field
// does not exist in data.
int age;
if (reader.getFieldKind("age") == FieldKind.INT32) {
age = reader.readInt32("age");
} else {
age = 0;
}
return new Employee(id, name, age);
}
...
}

Note that, when an old reader reads data written by an old writer, or a new reader reads a data written by a new writer, they will be able to read all fields written.

One thing to be careful while evolving the class is to not have any conditional code in the write method. That method must write all the fields available in the current version of the class to the writer, with appropriate field names and types. Hazelcast uses the write method of the serializer to extract a schema out of the object, hence any conditional code that may or may not run depending on the object in that method might result in an undefined behavior.

GenericRecord Representation​

Compact serialized objects can also be represented by a GenericRecord, without requiring the class in the classpath. See Accessing Domain Objects Without Domain Classes.

SQL Support​

Compact serialized objects can be used in SQL statements, provided that mappings are created, similar to other serialization formats. See Compact Object mappings section to learn more.

WAN Support​

Hazelcast supports WAN replication of the Compact serialized objects between different clusters.

Persistence Support​

Hazelcast supports persisting Compact serialized objects and reading the persisted data on startup.

Serialization Priority​

Compact serialization has the highest priority of all serialization mechanisms that are supported by Hazelcast. As a result, you can override other serialization mechanisms with Compact serialization.

That is especially useful when an interface signature forces you to implement other serialization mechanisms in Java. For example, you can define the following Employee class and still use the Compact serializer that you registered in your configuration.

java
public class Employee implements Serializable {
...
}
note

When using Jet pipelines, lambda expressions and certain pipeline components (such as EntryProcessor implementations) are serialized using Java serialization, which does not delegate to Hazelcast's serialization service. In these cases, captured objects must implement Serializable even if they have Compact serializers registered. For details, see Using Compact Serialization with Pipelines.

Compact Serialization Binary Specification​

The binary specification of compact serialization is publicly available at this page.