Examples
To run the examples, you need to clone ActiveJ from GitHub
git clone https://github.com/activej/activej
And import it as a Maven project. Check out tag v6.0-beta2. Before running the examples, build the project.
These examples are located at activej/examples/core/serializer
Simple Object Serialization
To create classes whose instances can be serialized/deserialized, special annotations must be used:
- @Serialize annotation with order number on property getter. Parameter
order
provides better compatibility if 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
public final int age;
@Serialize
public final String name;
private String surname;
@Serialize
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
}
Now let's do some serialization. We'll create an instance of a Person
class, a byte array that will store the result of serialization, and an instance of a BinarySerializer class, which represents the serializer that encodes and decodes <T>
values into byte arrays (in this case <Person>
values):
Person jim = new Person(34, "Jim");
jim.setSurname("Smith");
byte[] buffer = new byte[200];
BinarySerializer<Person> serializer = SerializerFactory.defaultInstance()
.create(Person.class);
That's it, now we can serialize and deserialize our Person
instance:
serializer.encode(buffer, 0, jim);
Person johnCopy = serializer.decode(buffer, 0);
Let's make a simple test to check if everything works correctly:
System.out.println(jim.age + " " + johnCopy.age);
System.out.println(jim.name + " " + johnCopy.name);
System.out.println(jim.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
Java record Serialization
ActiveJ Serializer supports serialization/deserialization of Java records out of the box. All you need to do is to mark your record with @SerializeRecord annotation like this:
@SerializeRecord
public record Person(int age, String name) {
}
Similarly to the previous example we'll create an instance of a Person
class, a byte array that will store the result of serialization, and an instance of a BinarySerializer class:
Person jim = new Person(34, "Jim");
byte[] buffer = new byte[200];
BinarySerializer<Person> serializer = SerializerFactory.defaultInstance()
.create(Person.class);
Now we can serialize and deserialize our Person
record instance:
serializer.encode(buffer, 0, jim);
Person johnCopy = serializer.decode(buffer, 0);
Let's make a simple test to check if everything works correctly:
System.out.println(jim.age + " " + johnCopy.age);
System.out.println(jim.name + " " + johnCopy.name);
System.out.println(jim.getSurname() + " " + johnCopy.getSurname());
After you run the example, you'll receive the following output:
34 34
Jim Jim
If you want to have more control over serialization of records, you can use most @Serialize...
annotations
(like @SerializeClass
, @SerializeNullable
, etc.) on record components.
You can explore full example sources on GitHub
Generics and Interfaces
ActiveJ Serializer can simply manage more complex objects. For example, let's see how it handles 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
public K getKey() {
return key;
}
@Serialize
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
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
@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
class, a
byte array to store the result of the serialization and an instance of a BinarySerializer<Developer>
serializer:
Developer developer = new Developer();
developer.setSkills(List.of(
new Skill<>(1, "Java"),
new Skill<>(2, "ActiveJ")));
byte[] buffer = new byte[200];
BinarySerializer<Developer> serializer = SerializerFactory.defaultInstance()
.create(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
Nested type serialization
Sometimes you need to serialize a field that represents a generic type. Let's say a Map
. All you need to do is put the @Serialize
annotation on that field.
@Serialize
public Map<Integer, String>> map;
But what if you want a map to contain nullable values? You can use the @SerializeNullable annotation. However, if you just put this annotation on the field, it would mean that a whole map could be nullable.
We have to put the annotation directly on a String
! Since ActiveJ v5.0, some serializer annotations are applicable to a type use.
So whenever you need to mark some type with additional serializer information, just put the annotation on a type:
@Serialize
public Map<Integer, @SerializeNullable String>> map;
In this tutorial we will show you how to write serializers using type use annotations.
We will define a parameterized class Nested
:
public static class Nested<T1, T2> {
@Serialize
public final T1 first;
@Serialize
public final T2 second;
public Nested(@Deserialize("first") T1 first, @Deserialize("second") T2 second) {
this.first = first;
this.second = second;
}
@Override
public String toString() {
return "Nested{" + first + ", " + second + '}';
}
}
Then we define a Storage
class to be serialized:
public static class Storage {
@Serialize
public List<@SerializeNullable Nested<Integer, @SerializeNullable String>> listOfNested;
}
The class has a single field, which is a List
of nullable Nested
elements. In addition, the second type parameter of the Nested
class (String
) is itself nullable.
We create a serializer as follows:
BinarySerializer<Storage> serializer = SerializerFactory.defaultInstance()
.create(Storage.class);
We then construct a Storage
class and add nullable elements to the list. Once we run the example we should see the following output:
[Nested{1, abc}, null, Nested{5, null}]
[Nested{1, abc}, null, Nested{5, null}]
This shows both an original Storage
contents as well as deserialized one.
A special care should be taken when annotating arrays.
@Foo String @Bar []
Here, @Foo
annotates String
while @Bar
annotates the whole array String[]
.
This is in accordance with Java Language Specification
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.
As you can see, you can write several annotations for the different paths of the same data structure.
- @SerializeFixedSize on properties that should have a fixed size after serialization
Let's create a simple example that illustrates how to use these annotations:
public static class Storage {
@Serialize
public @SerializeNullable String @SerializeFixedSize(3) [] strings;
@Serialize
public byte @SerializeFixedSize(4) [] bytes;
}
Now let's serialize and deserialize the Storage
instance similarly to the previous examples. We will create a Storage
instance, a byte array to store the serialization result, and a BinarySerializer<Storage>
serializer instance:
Storage storage = new Storage();
storage.strings = new String[]{"abc", null, "123", "superfluous"};
storage.bytes = new byte[]{1, 2, 3, 4, 5, 6};
byte[] buffer = new byte[200];
BinarySerializer<Storage> serializer = SerializerFactory.defaultInstance()
.create(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, 5, 6] -> [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
Custom serializer
In this example, we will demonstrate how to write a custom serializer for the LocalDate
class. You can use this example
as a reference for writing serializers for other classes that you might need to serialize.
Let's imagine we need to serialize a class that contains the LocalDate
field:
public static class LocalDateHolder {
@Serialize
public final LocalDate date;
public LocalDateHolder(@Deserialize("date") LocalDate date) {
this.date = date;
}
@Override
public String toString() {
return "LocalDateHolder{date=" + date + '}';
}
}
By default, the ActiveJ Serializer does not know how to serialize the LocalDate
class, so it will throw an exception when we
naively try to serialize it. We must provide a custom serializer for the LocalDate
class to serialize the LocalDateHolder
class:
public static class LocalDateSerializerDef extends SimpleSerializerDef<LocalDate> {
@Override
protected BinarySerializer<LocalDate> createSerializer(int version, CompatibilityLevel compatibilityLevel) {
return new BinarySerializer<>() {
@Override
public void encode(BinaryOutput out, LocalDate localDate) {
out.writeVarInt(localDate.getYear());
out.writeVarInt(localDate.getMonthValue());
out.writeVarInt(localDate.getDayOfMonth());
}
@Override
public LocalDate decode(BinaryInput in) throws CorruptedDataException {
int year = in.readVarInt();
int month = in.readVarInt();
int day = in.readVarInt();
return LocalDate.of(year, month, day);
}
};
}
}
We extend SimpleSerializerDef class and implement methods:
void encode(BinaryOutput out, LocalDate localDate)
- here we instruct the serializer how to serialize aLocalDate
instance. We actually need to serialize 3int
values (year
,month
, anddayOfMonth
) and write them to BinaryOutput as var ints (integers of variable length).LocalDate decode(BinaryInput in)
- here we need to instruct the serializer how to deserialize raw bytes into aLocalDate
instance. The process is an inverse to encoding. First, we have to read 3int
values (year
,month
, anddayOfMonth
) as var ints from BinaryInput. Then we can create a newLocalDate
instance by calling static factory methodLocalDate.of(year, month, dayOfMonth)
and passing previously deserializedint
values.
At last, we need to add our serializer of LocalDate
to SerializerFactory builder.
Alternatively, you can create a custom serializer by extending not a SimpleSerializerDef
class but a AbstractSerializerDef class.
You would need to implement encode()/decode()
methods that return Expression
. You would need to use Expression API in order to do so.
BinarySerializer<LocalDateHolder> serializer =
SerializerFactory.builder()
.with(LocalDate.class, ctx -> new LocalDateSerializerDef())
.build()
.create(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