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 declarations does not preserve identity #147

Closed
EdeMeijer opened this issue Apr 18, 2018 · 9 comments
Closed

Deserializing object declarations does not preserve identity #147

EdeMeijer opened this issue Apr 18, 2018 · 9 comments

Comments

@EdeMeijer
Copy link

When serializing and unserializing an object declaration, the resulting instance has a different identity than the original one, resulting in the objects being considered non-equal. For example:

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule

fun main(vararg args: String) {
    val mapper = ObjectMapper().registerKotlinModule()

    val successPre = Success(42)
    val successJson = mapper.writeValueAsString(successPre)
    val successPost = mapper.readValue(successJson, Success::class.java)

    println(successPre == successPost) // Prints true

    val failurePre = Failure
    val json = mapper.writeValueAsString(failurePre)
    val failurePost = mapper.readValue(json, Failure::class.java)

    println(failurePre == failurePost) // Prints false
}

object Failure

data class Success(val foo: Int)

I would expect Jackson to return the one and only singleton instance that Kotlin created for the object and have behaviour similar to data classes.


You might wonder why I'd try to JSON serialize an object declaration. My use case is dealing with polymorphism having a sealed class with 2 implementation; one is a data class and the other is an object, like this:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes(
    JsonSubTypes.Type(value = Purpose.StockOnHand::class, name = "stockOnHand"),
    JsonSubTypes.Type(value = Purpose.Fulfillment::class, name = "fulfillment")
)
sealed class Purpose {
    object StockOnHand : Purpose()
    data class Fulfillment(val channel: String) : Purpose()
}

which translates to JSON like

{"type": "stockOnHand"}

or

{"type": "fulfillment", "channel": "foo"}
@EdeMeijer
Copy link
Author

FYI, right now I'm using a workaround to have comparisons work as expected by overriding the equals method on the object declaration, like this

sealed class Purpose {
    object StockOnHand : Purpose() {
        // Hack for dealing with comparing objects after being JSON deserialized
        override fun equals(other: Any?): Boolean {
            return other is StockOnHand
        }
    }
    data class Fulfillment(val channel: String) : Purpose()
}

@nanodeath
Copy link

Another workaround, provided your object is always the thing being deserialized:

fun <T : Any> T.normalize(): T {
    this::class.objectInstance?.takeUnless { it === this }?.let { return it }
    return this
}

@EdeMeijer
Copy link
Author

@nanodeath certainly an option, might make it even easier to generically fix the issue in the json parsing code in case I introduce other objects later.

@apatrida
Copy link
Member

I'm not sure it is safe to have the Kotlin module deserialize something into the "one true instance" when it is an object, what if the contents are not the same? how would we even check that? The normalize function above is generic enough to work with any object. An object does not prevent you from having other instances of the same class definition, it just also acts as a singleton so generically handling this differently could be dangerous.

@gortiz
Copy link

gortiz commented Aug 28, 2018

Object declarations are singletons, so only one instance of these elements should be generated on a single JVM.
I can see there could be problems when deserializing a singleton. The same can happen when you are deserializing a enum literal. Enum literals can have state, but they should be identity comparables. How does Jackson resolve that? For consistency, I think the same solution should be used on Kotlin singletons

@MikeRippon
Copy link

I also have this problem.

@EdeMeijer
I think provided you don't mind having Jackson annotations in your code, this is a little cleaner/safer as it maintains the contract of there only ever being a single instance, rather than allowing Jackson to create multiple instances and hacking around equals.

sealed class Purpose {
    object StockOnHand : Purpose() {
        @JvmStatic @JsonCreator fun deserialize() = StockOnHand
    }
    data class Fulfillment(val channel: String) : Purpose()
}

@Gama11
Copy link

Gama11 commented Oct 1, 2020

It looks like this has actually been fixed in the meantime (see #225). Apparently there were some issues with it though (see #281) which led to it being an opt-in:

.registerModule(KotlinModule(singletonSupport = SingletonSupport.CANONICALIZE))

@borjab
Copy link

borjab commented Dec 20, 2021

If you have reached here thinking that there is a regression bug just notice that from version 2.11.0 onwards the behaviour has been disabled by default and needs to be activated with something like:

val objectMapper = ObjectMapper() .registerModule(KotlinModule(singletonSupport = CANONICALIZE))

@yonigibbs
Copy link

yonigibbs commented Jun 5, 2023

... or, in newer versions:

        mapper.registerModule(
            kotlinModule {
                configure(KotlinFeature.SingletonSupport, true)
            }
        )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants