Prism is a project that aims to create a powerful and very general Java serialization library. The core module contains the mechanisms that allow registering serializers and serializer factories for specific types, a system to reflectively analyze, access, and create arbitrary Java objects, as well as similar analyzers for collections and other common object types. Other than that it makes no assumptions about what serialization and deserialization look like for your format. It makes so few assumptions that the base serializer class doesn’t even contain any specific serialization functions.
Structure
Really the main two things the Prism core provides are the Prism
class and the set of “analyzers” that do the heavy lifting of serializing/deserializing complex objects. The Prism
class simply keeps track of all the serializers and deserializers, finds the most appropriate one for a given type, and uses serializer factories to create new ones if necessary. The main analyzer Prism provides is the ObjectAnalyzer
, which provides the capability to automatically serialize and deserialize arbitrary objects.
Prism
The foundation of the Prism project is, you guessed it, the Prism
class, which is what chooses the correct serializer for any given type. The Prism
accepts a single type argument, the type of Serializer
that your format uses. Serializer
and SerializerFactory
objects are then registered with the Prism
, and it will choose the most appropriate one automatically, first choosing the most specific Serializer
, and if none exists, using the most specific SerializerFactory
to create one.
val prism = Prism<JsonSerializer<*>>()
prism.register(ObjectSerializerFactory(prism))
prism.register(StringSerializer)
Serializers
The real meat of Prism is the Serializer
class, which is what implements the reading/writing behavior, however Serializer
itself doesn’t actually define any read/write methods. Instead, each format defines a custom abstract subclass of Serializer
which defines what reading/writing looks like for that format. For example, a JSON serializer may define a write method that returns a new JSON element, whereas a stream serializer may pass an output stream to the write method and have the serializer output directly to that without returning any value.
object StringSerializer: JsonSerializer<String>() {
override fun deserialize(element: JsonElement, existing: String?): String {
return (element as JsonPrimitive).getAsString()
}
override fun serialize(value: String): JsonElement {
return JsonPrimitive(value)
}
}
Serializer Factories
Serializer factories are used to create specialized serializers for generic types. For example, there would be a serializer for List<String>
, and another one for List<Integer>
, and yet another for List<List<String>>
. Each factory contains both a type pattern (e.g. List<?>
or Map<String, ?>
) and a predicate. The factory with the most specific matching type pattern will be used, provided its predicate returns true (this can be used, for example, to only use a factory for classes annotated with @RefractClass
).
open class Tagged<T>(var tag: String, var value: T)
class TaggedSerializerFactory(prism: JsonPrism): JsonSerializerFactory(prism, Mirror.reflect<Tagged<Any>>()) {
override fun create(mirror: TypeMirror): JsonSerializer<*> {
return TaggedSerializer(prism, mirror as ClassMirror)
}
class TaggedSerializer(prism: JsonPrism, type: ClassMirror): JsonSerializer<Tagged<Any>>(type) {
val valueSerializer by prism[type.findSuperclass(Tagged::class.java).typeParameters[0]]
override fun deserialize(element: JsonElement, existing: Tagged<Any>?): Tagged<Any> {
element as JsonObject
return Tagged(
element["tag"].getAsString(),
valueSerializer.deserialize(element["value"], existing?.value)
)
}
override fun serialize(value: Tagged<Any>): JsonElement {
val element = JsonObject()
element.add("tag", JsonPrimitive(value.tag))
element.add("value", valueSerializer.serialize(value.value))
return element
}
}
}
Automatic Serialization
Unlike many serialization libraries, by default Prism avoids using “magic” to access data, striving to access it through normal mechanisms, including using getters and setters, respecting immutable properties, and using constructors to create new instances and to “modify” immutable properties of existing instances. Naturally, these considerations can be disabled for any property if necessary.
@RefractClass
public class AutoSerializedType {
// Prism can access private fields
@Refract
private List<String> plainField;
private int accessorValue;
private String accessorString;
// Prism understands getters and setters
@RefractGetter("accessorValue")
public int getAccessorValue() {
return this.accessorValue;
}
@RefractSetter("accessorValue")
public void setAccessorValue(int value) {
this.accessorValue = value;
this.accessorString = "Value is: " + value;
}
@Refract
private final int finalField;
// Prism will automatically detect when immutable properties' values have changed,
// and can create a new instance with the changed values. This will also be used
// when there is no existing value, allowing Prism to create objects from scratch.
@RefractConstructor
public AutoSerializedType(int finalField) {
this.finalField = finalField;
}
}
Prism’s automatic serialization is very easy for formats to interface with. Once the ObjectAnalyzer
is created, the serialization and deserialization processes are very simple, with the analyzer doing all the heavy lifting.
open class ObjectSerializerFactory(prism: JsonPrism): JsonSerializerFactory(prism, Mirror.reflect<Any>(), {
(it as ClassMirror).annotations.any { it is RefractClass }
}) {
override fun create(mirror: TypeMirror): JsonSerializer<*> {
return ObjectSerializer(prism, mirror)
}
class ObjectSerializer(prism: JsonPrism, type: TypeMirror): JsonSerializer<Any>(type) {
val analyzer = ObjectAnalyzer<Any, JsonSerializer<*>>(prism, type.asClassMirror())
override fun deserialize(element: JsonElement, existing: Any?): Any {
if(element !is JsonObject) throw DeserializationException("Object serializer expects an object element")
analyzer.getReader(existing).use { reader ->
reader.properties.forEach { property ->
property.value = element[property.name]?.let {
property.serializer.read(it, property.existing)
}
}
return reader.apply()
}
}
override fun serialize(value: Any): JsonElement {
val element = JsonObject()
analyzer.getWriter(value).use { writer ->
writer.properties.forEach { property ->
val propertyValue = property.value
if(propertyValue != null)
element[property.name] = property.serializer.write(propertyValue)
}
}
return element
}
}
}