How to serialize class with polymorphic property using micronaut-serialization and annotations?

Viewed 294

I followed this guide to use Micronaut Serialization with Jackson annotations. When I create an abstract Base class and a Derived class, I can use @JsonTypeInfo and @JsonSubTypes to correctly serialize objects of the Derived class.

However, when I serialize an object of OtherClass with a property of type Base but with runtime type Derived, the property is serialized as Base.

The code below should explain my problem. Note that I am using io.micronaut.serde.ObjectMapper, when I replace it with com.fasterxml.jackson.databind.ObjectMapper it works as expected.

Is there a way to make it work using annotations?

import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.string.shouldContain
import io.micronaut.serde.ObjectMapper
import io.micronaut.serde.annotation.Serdeable
import io.micronaut.test.extensions.kotest.annotation.MicronautTest

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "class")
@JsonSubTypes(JsonSubTypes.Type(Derived::class))
@Serdeable
abstract class Base(val baseProperty: String = "baseProperty")

@Serdeable
class Derived : Base()

@Serdeable
class OtherClass(val otherProperty: Base)

@MicronautTest
class SerializeTest(objectMapper: ObjectMapper) : ShouldSpec({
    should("serialize Derived") {
        val derived: Base = Derived()
        val valueAsString = objectMapper.writeValueAsString(derived)

        /**
         * succeeds:
         * valueAsString = "{"class":"com.example.Derived","baseProperty":"baseProperty"}"
         */
        valueAsString shouldContain "class"
    }

    should("serialize otherProperty as Derived") {
        val derived = Derived()
        val otherClass = OtherClass(derived)
        val valueAsString = objectMapper.writeValueAsString(otherClass)

        /**
         * fails:
         * valueAsString = "{"otherProperty":{"baseProperty":"baseProperty"}}"
         * expected: valueAsString = "{"otherProperty":{"class":"com.example.Derived","baseProperty":"baseProperty"}}"
         */
        valueAsString shouldContain "class"
    }
})
1 Answers

I have not used Micronaut Serialization but have used classical Jackson for Kotlin with @JsonTypeInfo and @JsonSubTypes and this works properly for a top object or embedded object like you have in OtherClass.

In one of my use cases, it took me a long while to find this attribute being necessary on the @JsonTypeInfo annotation:

visible = true

So I am not claiming this as a solution, but the visible attribute documentation says

Default value is false, meaning that Jackson handles and removes the type identifier from JSON content that is passed to JsonDeserializer.

So perhaps this is relevant so the inner object's type is not wiped for view higher up in the object graph??

Related