ActiveJ Serializer Examples

Note: You have two options to run the examples.
Option 1: Create a new project, import ActiveJ Serializer Maven repository and recreate the examples from scratch.
Option 2: clone ActiveJ project from GitHub ($ git clone https://github.com/activej/activej)
and import it as a Maven project. Check out tag v4.2 and build the project. The examples are located at activej -> examples -> core -> serializer

Simple Object Serialization

In order to create classes whose instances can be serialized/deserialized, you should use special annotations:

  • @Serialize annotation with order number on property getter. Parameter order provides better compatibility in case classes are changed.
  • @Deserialize annotation with property name (which should be the same name as the one in getter) in constructor.

This is enough to create serializable POJOs, for example:

public static class Person {
	public Person(@Deserialize("age") int age,
				  @Deserialize("name") String name) {
		this.age = age;
		this.name = name;
	}

	@Serialize(order = 0)
	public final int age;

	@Serialize(order = 1)
	public final String name;

	private String surname;

	@Serialize(order = 2)
	public String getSurname() {
		return surname;
	}

	public void setSurname(String surname) {
		this.surname = surname;
	}
}

Now let’s do some serialization. We’ll create a Person instance, a byte array that stores the result of the serialization, and a BinarySerializer instance that represents a serializer that encodes and decodes <T> values to byte arrays (<Person> values in this case):

Person john = new Person(34, "Jim");
john.setSurname("Smith");
byte[] buffer = new byte[200];
BinarySerializer<Person> serializer = SerializerBuilder.create()
		.build(Person.class);

That’s it, now we can serialize and deserialize our Person instance:

serializer.encode(buffer, 0, john);
Person johnCopy = serializer.decode(buffer, 0);

Let’s make a simple test to check if everything works correctly:

System.out.println(john.age + " " + johnCopy.age);
System.out.println(john.name + " " + johnCopy.name);
System.out.println(john.getSurname() + " " + johnCopy.getSurname());

After you run the example, you’ll receive the following output:

34 34
Jim Jim
Smith Smith

Which means that the serialization and deserialization worked correctly.

You can explore full example sources on GitHub.

Generics and Interfaces Serialization

ActiveJ Serializer can simply manage more complex objects. For example, let’s see how it works with interfaces and generics.

First, create a simple Skill class:

public static class Skill<K, V> {
	private final K key;
	private final V value;

	public Skill(@Deserialize("key") K key,
				 @Deserialize("value") V value) {
		this.key = key;
		this.value = value;
	}

	@Serialize(order = 0)
	public K getKey() {
		return key;
	}

	@Serialize(order = 1)
	public V getValue() {
		return value;
	}
}

Next, create a Person interface that has a single method returning a list of skills:

public interface Person<K, V> {
	@Serialize(order = 0)
	List<Skill<K, V>> getSkills();
}

Finally create a Developer class that implements Person interface:

public static class Developer implements Person<Integer, String> {
	private List<Skill<Integer, String>> list;

	@Serialize(order = 0)
	@Override
	public List<Skill<Integer, String>> getSkills() {
		return list;
	}

	public void setSkills(List<Skill<Integer, String>> list) {
		this.list = list;
	}
}

Let’s proceed to the serialization. Similarly to the previous example, we’ll create an instance of the Developer, a byte array to store the result of the serialization and an instance of BinarySerializer<Developer> serializer:

Developer developer = new Developer();
developer.setSkills(Arrays.asList(
		new Skill<>(1, "Java"),
		new Skill<>(2, "ActiveJ")));

byte[] buffer = new byte[200];
BinarySerializer<Developer> serializer = SerializerBuilder.create()
		.build(Developer.class);

Now let’s serialize and deserialize our Developer instance:

serializer.encode(buffer, 0, developer);
Developer developer2 = serializer.decode(buffer, 0);

Check if the serialization works correctly:

for (int i = 0; i < developer.getSkills().size(); i++) {
	System.out.println(developer.getSkills().get(i).getKey() + " - " + developer.getSkills().get(i).getValue() +
			", " + developer2.getSkills().get(i).getKey() + " - " + developer2.getSkills().get(i).getValue());
}

If you run the example, you’ll receive the following output:

1 - Java, 1 - Java
2 - ActiveJ, 2 - ActiveJ

Which means that the serialization worked correctly.

You can explore full example sources on GitHub.

Fixed Size and Nullable Fields Serialization

ActiveJ Serializer has some helper annotations, for example:

  • @SerializeNullable on properties that can have null values. This annotation also has a special path parameter. It represent a path of the tree of the variable’s data types. It allows to indicate which of the ‘nodes’ is nullable.

For example:

@Serialize(order = 0)
@SerializeNullable() // refers to Map<String, Map<Integer, Float[]>>
@SerializeNullable(path = {1}) // refers to Map<Integer, Float[]
@SerializeNullable(path = {0}) // refers to String
@SerializeNullable(path = {1, 0}) // refers to Integer
@SerializeNullable(path = {1, 1}) // refers to Float[]
@SerializeNullable(path = {1, 1, 0}) //refers to the Float elements of the array
public Map<String, Map<Integer, Float[]>> complexMap;

As you can see, you can write several annotations for the different paths of the same data structure.

Let’s create a simple example that illustrates how to use these annotations:

public static class Storage {
	@Serialize(order = 0)
	@SerializeFixedSize(3)
	@SerializeNullable(path = {0})
	public String[] strings;

	@Serialize(order = 1)
	@SerializeFixedSize(4)
	public byte[] bytes;
}

Now let’s serialize and deserialize an instance of the Storage similarly to the previous examples. We’ll create an instance of the Storage, a byte array to store the result of the serialization and an instance of BinarySerializer<Storage> serializer:

Storage storage = new Storage();
storage.strings = new String[]{"abc", null, "123", "superfluous"};
storage.bytes = new byte[]{1, 2, 3, 4};

byte[] buffer = new byte[200];
BinarySerializer<Storage> serializer = SerializerBuilder.create()
		.build(Storage.class);

Finally, serialize and deserialize Storage instance:

serializer.encode(buffer, 0, storage);
Storage limitedStorage = serializer.decode(buffer, 0);

Let’s see how serialization affected the storage:

System.out.println(Arrays.toString(storage.strings) + " -> " + Arrays.toString(limitedStorage.strings));
System.out.println(Arrays.toString(storage.bytes) + " -> " + Arrays.toString(limitedStorage.bytes));

If you run the example, you’ll see the following output:

[abc, null, 123, superfluous] -> [abc, null, 123]
[1, 2, 3, 4] -> [1, 2, 3, 4]

As you can see in the first line, storage differs from limitedStorage. This is because @SerializeFixedSize annotation was set at value 3 for the strings property. Thus, “superfluous” was removed from the array while serialization took place.

You can explore full example sources on GitHub.

Writing a custom serializer

In this example, we will demonstrate how you can write a custom serializer for a LocalDate class. You can use this example as a reference for writing serializers for other classes that you may need to serialize.

Let’s imagine we need to serialize a class that contains LocalDate field:

public static class LocalDateHolder {
	@Serialize(order = 0)
	public final LocalDate date;

	public LocalDateHolder(@Deserialize("date") LocalDate date) {
		this.date = date;
	}

	@Override
	public String toString() {
		return "LocalDateHolder{date=" + date + '}';
	}
}

By default, ActiveJ Serializer does not know how to serialize a LocalDate class, so it would throw an exception if you naively try to serialize it. We have to provide a custom serializer for a LocalDate class to serialize LocalDateHolder class:

public static class SerializerDefLocalDate extends AbstractSerializerDef {

	@Override
	public Class<?> getEncodeType() {
		return LocalDate.class;
	}

	@Override
	public Expression encoder(final StaticEncoders staticEncoders,
							  final Expression buf,
							  final Variable pos,
							  final Expression localDate,
							  final int version,
							  final CompatibilityLevel compatibilityLevel) {
		return sequence(
				writeVarInt(buf, pos, call(localDate, "getYear")),
				writeVarInt(buf, pos, call(localDate, "getMonthValue")),
				writeVarInt(buf, pos, call(localDate, "getDayOfMonth"))
		);
	}

	@Override
	public Expression decoder(final StaticDecoders staticDecoders,
							  final Expression input,
							  final int version,
							  final CompatibilityLevel compatibilityLevel) {
		return staticCall(LocalDate.class, "of",
				readVarInt(input),
				readVarInt(input),
				readVarInt(input)
		);
	}
}

We extend AbstractSerializerDef class and implement methods:

  • Class<?> getEncodeType() - specify LocalDate.class as the type of data to be serialized
  • Expression encoder(...) - here we instruct the serializer how to serialize a LocalDate instance. We actually need to serialize 3 int values (year, month, and dayOfMonth) and write them to BinaryOutput. Using Lisp-like Expression API we do just that.
  • Expression decoder(...) - here we need to instruct the serializer how to deserialize raw bytes into a LocalDate instance. The process is an inverse to encoding. First, we have to read 3 int values (year, month, and dayOfMonth) from BinaryInput. Then we can create a new LocalDate instance by calling static factory method static LocalDate of(int year, int month, int dayOfMonth) and passing previously deserialized int values. We once again use Expression API for this task.

At last, we need to add our serializer of LocalDate to SerializerBuilder:

BinarySerializer<LocalDateHolder> serializer =
		SerializerBuilder.create()
				.withSerializer(LocalDate.class, new SerializerDefLocalDate())
				.build(LocalDateHolder.class);

If we run LocalDateSerializerExample#main method, we should see the following output:

Serializing LocalDateHolder: LocalDateHolder{date=2021-03-17}
Byte array with serialized LocalDateHolder: [-27, 15, 3, 17]
Deserialized LocalDateHolder: LocalDateHolder{date=2021-03-17}

You can explore full example sources on GitHub.