Missing identity field with polymorphic (de)serialisation in Kotlin with Jackson

1.9k views Asked by At

I have the following class hierarchy annotated as such:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes(
    JsonSubTypes.Type(value = NetCommand.AddEntity::class, name = "AddEntity"),
    JsonSubTypes.Type(value = NetCommand.RemoveEntity::class, name = "RemoveEntity"),
    JsonSubTypes.Type(value = NetCommand.MoveEntity::class, name = "MoveEntity"),
    JsonSubTypes.Type(value = NetCommand.SpeakEntity::class, name = "SpeakEntity"),
    JsonSubTypes.Type(value = NetCommand.AddItem::class, name = "AddItem")
)
sealed class NetCommand {
    class AddEntity(val id: Long, val position: TilePosition, val table: Character) : NetCommand()
    class RemoveEntity(val id: Long) : NetCommand()
    class MoveEntity(val id: Long, val position: TilePosition) : NetCommand()
    class SpeakEntity(val id: Long, val username: String, val message: String) : NetCommand()
    class AddItem(val id: Long, val item: Item) : NetCommand()
}

The idea being I can communicate a collection (ArrayList) of NetCommand to a second application and have them be correctly deserialised into the appropriate subclass.

I have also written a simple test to help me iterate on different configurations of the annotations/jackson mapper:

val command = NetCommand.AddEntity(1, TilePosition(0, 0), Character.KNIGHT)
val commandList: ArrayList<NetCommand> = ArrayList()
commandList.add(command)

val mapper = jacksonObjectMapper()

val commandListString = mapper.writeValueAsString(commandList)
val resultList = mapper.readValue<ArrayList<NetCommand>>(commandListString)

assert(resultList[0] as? NetCommand.AddEntity != null)
assert((resultList[0] as NetCommand.AddEntity).id == command.id)

This fails on the line:

val resultList = mapper.readValue<ArrayList<NetCommand>>(commandListString)

With this error:

Missing type id when trying to resolve subtype of [simple type, class shared.NetCommand]: missing type id property 'type'
 at [Source: (String)"[{"id":1,"position":{"x":0,"y":0},"table":"KNIGHT"}]"; line: 1, column: 51] (through reference chain: java.util.ArrayList[0])
com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class shared.NetCommand]: missing type id property 'type'
 at [Source: (String)"[{"id":1,"position":{"x":0,"y":0},"table":"KNIGHT"}]"; line: 1, column: 51] (through reference chain: java.util.ArrayList[0])

Any ideas why my type field isn't being serialised?


(Less than ideal) Solution

I found a solution in manually adding an already initialised field to the body of subclasses with the name of the subclass. Eg.

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes(
    JsonSubTypes.Type(value = AddEntity::class, name = "AddEntity"),
    JsonSubTypes.Type(value = RemoveEntity::class, name = "RemoveEntity"),
    JsonSubTypes.Type(value = MoveEntity::class, name = "MoveEntity"),
    JsonSubTypes.Type(value = SpeakEntity::class, name = "SpeakEntity"),
    JsonSubTypes.Type(value = AddItem::class, name = "AddItem")
)
sealed class NetCommand { val type: String = javaClass.simpleName }
class AddEntity(val id: Long, val position: TilePosition, val table: Character) : NetCommand()
class RemoveEntity(val id: Long) : NetCommand()
class MoveEntity(val id: Long, val position: TilePosition) : NetCommand()
class SpeakEntity(val id: Long, val username: String, val message: String) : NetCommand()
class AddItem(val id: Long, val item: Item) : NetCommand()

Ideally I'd like to just use the simple class name automatically rather than having name = "AddEntity" etc. on each JsonSubTypes.Type call.

2

There are 2 answers

1
dave On BEST ANSWER

I think I've found the best solution I'm gonna find. Using the JsonTypeInfo.Id.CLASS for the mapping I no longer need to provide names for each subtype - it just relies on the fully qualified class name. This automatically uses the field name @class which I can automatically populate on the super class NetCommand using the @JsonProperty annotation to name the field correctly. Also worth noting is we don't need to provide the @JsonSubTypes annotation at all.

Would rather be using the SimpleName (eg. AddItem instead of my.fully.qualified.path.AddItem) but haven't figured that out yet.

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
sealed class NetCommand { @JsonProperty("@class") val type = javaClass.canonicalName }
class AddEntity(val id: Long, val position: TilePosition, val table: Character) : NetCommand()
class RemoveEntity(val id: Long) : NetCommand()
class MoveEntity(val id: Long, val position: TilePosition) : NetCommand()
class SpeakEntity(val id: Long, val username: String, val message: String) : NetCommand()
class AddItem(val id: Long, val item: Item) : NetCommand()
0
dmitry On

As an addition to the OP's solution and ryfterek comment, the following annotation would have taken care of explicitly declaring, mentioned @JsonProperty("@class") val type = javaClass.canonicalName property: @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "type"). Where 'type' is the name of the field that will be declared in POJO.