Custom JSON serialization for cyclically dependent data classes in Kotlin

Viewed 32

Imagine we have two data classes Car and CarDoor

data class Car(val doors: List<CarDoor>, val color: String, val maxSpeed: Double)
data class CarDoor(val belongsTo: Car, var isOpen: Boolean)

(Sadly Kotlin does not allow CarDoor to be both an inner class and @Serializable, so I had to pick my battles)

How would we go about using @Serializable to (de)serialize these two classes correctly, without encountering StackOverflowException since they are cyclically related.

Basically, how do I make the belongsTo field of CarDoor transient so that it does not participate in serialization? @Transient does not work because then I'd have to assign an initializing expression to that field.


Edit: the JSON produced should look something like this:

{"doors": [
    {"isOpen": false},
    {"isOpen": false},
    {"isOpen": false},
    {"isOpen": true}
], "color": "red", "maxSpeed": 95}

or, and this is even better, to have an identifier of the car inside each car-door, like this:

{"name": "cool-car-1",
"doors": [
    {"car": "cool-car-1", "isOpen": false},
    {"car": "cool-car-1", "isOpen": false},
    {"car": "cool-car-1", "isOpen": false},
    {"car": "cool-car-1", "isOpen": true}
], "color": "red", "maxSpeed": 95}
1 Answers

What you want, is not possible with standard kotlin serialization. It's just math: when belongsTo is @Transient, the object can't be deserialized when a nonnull property belongsTo is not included. So you have to either code the (de)serialization by hand or change your data model.

Without further knowledge of your actual code requirements, I can only make a suggestion on the latter, not sure if it is acceptable for you:

@Serializable
data class CarDoor(
    @Transient
    var belongsTo: Car? = null,
    var isOpen: Boolean,
)

This way, serialization should produce your first json example. On deserialization however, the belongsTo property will be null. You would have to fill the car by hand immediately after deserialisation. Probably this is as easy as looping through the list like this:

val car = Json....
car.doors.forEach { it.belongsTo = car }

Finally, if you can't live with a nullable belongsTo property, create two extra data classes that you use for serialization and deserialization. That will require extra steps before serialization and after deserialization to translate to/from the serializable classes. Only in your serializable class, belongsTo needs to be nullable.

Related