Prism

View project on GitHub

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&lt;String&gt;, and another one for List&lt;Integer&gt;, and yet another for List&lt;List&lt;String&gt;&gt;. Each factory contains both a type pattern (e.g. List&lt;?&gt; or Map&lt;String, ?&gt;) 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

WORK IN PROGRESS

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
        }
    }
}

WORK IN PROGRESS