Default Class Evolution

Corda’s serialization framework supports minor modifications to default classes without requiring external modification or annotation. You can:

  • Add nullable properties.
  • Add non-nullable properties if you also provide an annotated constructor.
  • Remove properties.
  • Reorder constructor parameters.

You can add nullable properties without any additional code. For example:

// Initial instance of the class
data class Example1 (val a: Int, b: String) // (Version A)

// Class post addition of property c
data class Example1 (val a: Int, b: String, c: Int?) // (Version B)

If a node has version A of class Example1, it can deserialize a blob that has been serialized by a node with version B of Example1. The framework treats it as a removed property.

A node with class Example1 at version B can deserialize a serialized version A of Example1 without any modification. The property is nullable, so it provides null to the constructor.

If you add a non-nullable property, you need to add some additional code:

// Initial instance of the class
data class Example2 (val a: Int, b: String) // (Version A)

// Class post addition of property c
data class Example1 (val a: Int, b: String, c: Int) { // (Version B)
     @DeprecatedConstructorForDeserialization(1)
     constructor (a: Int, b: String) : this(a, b, 0) // 0 has been determined as a sensible default
}

For this example to work, you must add a new constructor. The constructor allows nodes that have the class at version B to create an instance from the serialized form of that class in an older version (version A). The example provides A sensible default for the missing value is provided for instantiation of the non-null property.

As with nullable properties, if a node has version A of class Example1, it can deserialize a blob that has been serialized by a node with version B of Example1. It treats them as if the property has been removed.

If you add multiple non-nullable properties over time, then a class may need to deserialize several forms of the class. Select the correct constructor to maximize information extraction.

Consider this example:

// The original version of the class
data class Example3 (val a: Int, val b: Int)
// The first alteration, property c added
data class Example3 (val a: Int, val b: Int, val c: Int)
// The second alteration, property d added
data class Example3 (val a: Int, val b: Int, val c: Int, val d: Int)
// The third alteration, and how it currently exists, property e added
data class Example3 (val a: Int, val b: Int, val c: Int, val d: Int, val: Int e) {
    // NOTE: version number purposefully omitted from annotation for demonstration purposes
    @DeprecatedConstructorForDeserialization
    constructor (a: Int, b: Int) : this(a, b, -1, -1, -1)          // alt constructor 1
    @DeprecatedConstructorForDeserialization
    constructor (a: Int, b: Int, c: Int) : this(a, b, c, -1, -1)   // alt constructor 2
    @DeprecatedConstructorForDeserialization
    constructor (a: Int, b: Int, c: Int, d) : this(a, b, c, d, -1) // alt constructor 3
}

In this case, the deserializer must deserialize instances of class Example3 that were serialized as:

Example3 (1, 2)             // example I
Example3 (1, 2, 3)          // example II
Example3 (1, 2, 3, 4)       // example III
Example3 (1, 2, 3, 4, 5)    // example IV

Examples I, II, and III require evolution, so you need to select a constructor for them. Here, it’s difficult to tell which constructor to use because there is no versioning. For example, example II could use ‘alt constructor 2’ which matches its arguments most tightly. It could also use ‘alt constructor 1’ and not instantiate parameter c:

constructor (a: Int, b: Int, c: Int) : this(a, b, c, -1, -1)

or

constructor (a: Int, b: Int) : this(a, b, -1, -1, -1)

You can remove this ambiguity by adding version numbers to the constructor annotation. This gives a strict precedence order to the constructor selection:

// The third alteration, and how it currently exists, property e added
data class Example3 (val a: Int, val b: Int, val c: Int, val d: Int, val: Int e) {
    @DeprecatedConstructorForDeserialization(1)
    constructor (a: Int, b: Int) : this(a, b, -1, -1, -1)          // alt constructor 1
    @DeprecatedConstructorForDeserialization(2)
    constructor (a: Int, b: Int, c: Int) : this(a, b, c, -1, -1)   // alt constructor 2
    @DeprecatedConstructorForDeserialization(3)
    constructor (a: Int, b: Int, c: Int, d) : this(a, b, c, d, -1) // alt constructor 3
}

The framework selects constructors in descending order, until one enables construction. Deserializing examples I to IV would result in:

Example3 (1, 2, -1, -1, -1) // example I
Example3 (1, 2, 3, -1, -1)  // example II
Example3 (1, 2, 3, 4, -1)   // example III
Example3 (1, 2, 3, 4, 5)    // example IV

Removing non-nullable properties is impossible. If a node receives a message containing a serialized form of an object that has fewer properties than it requires for construction, it can’t determine sensible defaults.

It is technically possible to remove nullable properties in some circumstances, but it often has unintended consequences. Deprecate the field instead.

You can reorder properties (in Kotlin, this corresponds to constructor parameters) freely. The evolution serializer maps the class’s serialization to its current constructor parameter order. This is important to our AMQP framework as it constructs objects using their primary (or annotated) constructor. The ordering of that constructor’s parameters determine the way an object’s properties were serialized into the byte stream.

For an illustrative example, consider a simple class:

data class Example5 (val a: Int, val b: String)

val e = Example5(999, "hello")

When you serialize e, its properties are encoded in the order of its primary constructor’s parameters:

999,hello

If you reorder those parameters post-serialization, then deserializing without evolution will fail with a basic type error. This is because the framework would attempt to create the new value of Example5 with the values provided in the wrong order:

// changed post serialisation
data class Example5 (val b: String, val a: Int)
| 999 | hello |  <--- Extract properties to pass to constructor from byte stream
   |      |
   |      +--------------------------+
   +--------------------------+      |
                              |      |
deserializedValue = Example5(999, "hello")  <--- Resulting attempt at construction
                              |      |
                              |      \
                              |       \     <--- Will clearly fail as 999 is not a
                              |        \         string and hello is not an integer
data class Example5 (val b: String, val a: Int)

Learn more about:

Was this page helpful?

Thanks for your feedback!

Chat with us

Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.

Propose documentation improvements directly

Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.

We're sorry this page wasn't helpful. Let us know how we can make it better!

Chat with us

Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.

Create an issue

Create a new GitHub issue in this repository - submit technical feedback, draw attention to a potential documentation bug, or share ideas for improvement and general feedback.

Propose documentation improvements directly

Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.