Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deserializing object to data class that contains delegated properties (inheritance with delegation) #471

Closed
pnuckowski opened this issue Jun 24, 2021 · 2 comments
Labels

Comments

@pnuckowski
Copy link

pnuckowski commented Jun 24, 2021

Your question
I have checked issues and docs and could not find solution.

The follwing code properly serialize object (Component) to string, but deserialization from string back to insgtances of Component doesn't work.

Any suggestions how to do it, without removing delegations?

Thanks :)

import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonSubTypes.Type
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "type"
)
@JsonSubTypes(
    value = [
        Type(value = Space::class, name = Space.TYPE),
        Type(value = Command::class, name = Command.TYPE),
    ]
)
interface Component {
    var id: String

    @get:JsonIgnore
    val type: String
}

data class Command(
    val value: String,
    val component: Component
) : Component by component {
    override val type: String
        get() = TYPE

    companion object {
        const val TYPE = "command"
    }
}

data class Space(
    val space: String,
    private val component: Component
) : Component by component {
    override val type: String
        get() = TYPE

    companion object {
        const val TYPE = "space"
    }
}

data class Info(
    override var id: String,
    override val type: String = "",
) : Component

class FooTest {
    //    private val objectMapper = ObjectMapperFactory.createObjectMapper(emptyList())
    private val objectMapper = JsonMapper.builder()
        .addModule(KotlinModule())
        .serializationInclusion(JsonInclude.Include.NON_NULL)
        .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
        .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)
        .enable(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER)
        .build()

    @Test
    fun `should deserialize`() {

        val value = objectMapper.writeValueAsString(
            Command(
                value = "text",
                component = Info(id = "d")
            )
        )

        val obj = objectMapper.readValue(value, Component::class.java)
        assertThat(value).isEqualTo(obj)
    }
}

Error


com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'Info' as a subtype of `<>.<>.<>.<>.jackson.Component`: known type ids = [command, space] (for POJO property 'component')
 at [Source: (String)"{"type":"command","value":"text","component":{"type":"Info","id":"d"},"id":"d"}"; line: 1, column: 54] (through reference chain: <>.<>.<>.<>.jackson.Command["component"])
@dinomite
Copy link
Member

I don't this has anything to do with delegation, but please let me know if I've misunderstood.

The Info class doesn't have an entry in the @JsonSubTypes list, so Jackson doesn't know what it is. Here is a simplified version of your example that works, mainly by adding that entry for Info:

import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonSubTypes.Type
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.annotation.JsonTypeInfo.As
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id
import com.fasterxml.jackson.module.kotlin.jsonMapper
import com.fasterxml.jackson.module.kotlin.kotlinModule
import org.junit.Assert.assertEquals
import org.junit.Test

@JsonTypeInfo(
    use = Id.NAME,
    include = As.PROPERTY,
    property = "typeInfo"
)
@JsonSubTypes(
    value = [
        Type(value = Command::class, name = Command.TYPE),
        Type(value = Info::class, name = "info")
    ]
)
interface Component {
    var id: String

    @get:JsonIgnore
    val type: String
}

data class Command(
    val value: String,
    val component: Component
) : Component by component {
    override val type: String
        get() = TYPE

    companion object {
        const val TYPE = "command"
    }
}

data class Info(
    override var id: String,
    override val type: String = "",
) : Component

class FooTest {
    private val objectMapper = jsonMapper { addModule(kotlinModule()) }

    @Test
    fun `should deserialize`() {
        val expected = Command(value = "text", component = Info(id = "d"))
        val value = objectMapper.writerWithDefaultPrettyPrinter()
            .writeValueAsString( expected )
        println(value)

        assertEquals(expected, objectMapper.readValue(value, Component::class.java))
    }
}

Note that Jackson can also attempt to determine the type to deserialize into based upon the fields present using deduction.

@k163377
Copy link
Contributor

k163377 commented Apr 4, 2023

This issue is closed because it has been answered.

@k163377 k163377 closed this as completed Apr 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants