diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinBeanDeserializerModifier.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinBeanDeserializerModifier.kt new file mode 100644 index 000000000..eaf427163 --- /dev/null +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinBeanDeserializerModifier.kt @@ -0,0 +1,26 @@ +package com.fasterxml.jackson.module.kotlin + +import com.fasterxml.jackson.databind.BeanDescription +import com.fasterxml.jackson.databind.DeserializationConfig +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier + +object KotlinBeanDeserializerModifier: BeanDeserializerModifier() { + override fun modifyDeserializer( + config: DeserializationConfig, + beanDesc: BeanDescription, + deserializer: JsonDeserializer<*> + ) = super.modifyDeserializer(config, beanDesc, deserializer) + .maybeSingletonDeserializer(objectSingletonInstance(beanDesc.beanClass)) +} + +fun JsonDeserializer<*>.maybeSingletonDeserializer(singleton: Any?) = when (singleton) { + null -> this + else -> this.asSingletonDeserializer(singleton) +} + +private fun objectSingletonInstance(beanClass: Class<*>): Any? = if (!beanClass.isKotlinClass()) { + null +} else { + beanClass.kotlin.objectInstance +} \ No newline at end of file diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt index eba3db603..637a156ee 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt @@ -43,6 +43,8 @@ class KotlinModule(val reflectionCacheSize: Int = 512, val nullToEmptyCollection context.addValueInstantiators(KotlinInstantiators(cache, nullToEmptyCollection, nullToEmptyMap)) + context.addBeanDeserializerModifier(KotlinBeanDeserializerModifier) + fun addMixIn(clazz: Class<*>, mixin: Class<*>) { context.setMixInAnnotations(clazz, mixin) } diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinObjectSingletonDeserializer.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinObjectSingletonDeserializer.kt new file mode 100644 index 000000000..05c3daaf4 --- /dev/null +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinObjectSingletonDeserializer.kt @@ -0,0 +1,42 @@ +package com.fasterxml.jackson.module.kotlin + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.BeanProperty +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.deser.ContextualDeserializer +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer + +internal fun JsonDeserializer<*>.asSingletonDeserializer(singleton: Any) = + KotlinObjectSingletonDeserializer(singleton, this) + +/** deserialize as normal, but return the canonical singleton instance. */ +internal class KotlinObjectSingletonDeserializer( + private val singletonInstance: Any, + private val defaultDeserializer: JsonDeserializer<*> +) : JsonDeserializer(), + // Additional interfaces of a specific 'JsonDeserializer' must be supported + // Kotlin objectInstances are currently handled by a BeanSerializer which + // implements 'ContextualDeserializer' and 'ResolvableDeserializer'. + ContextualDeserializer, + ResolvableDeserializer { + + override fun resolve(ctxt: DeserializationContext?) { + if (defaultDeserializer is ResolvableDeserializer) { + defaultDeserializer.resolve(ctxt) + } + } + + override fun createContextual(ctxt: DeserializationContext?, property: BeanProperty?): JsonDeserializer<*> = + if (defaultDeserializer is ContextualDeserializer) { + defaultDeserializer.createContextual(ctxt, property) + .asSingletonDeserializer(singletonInstance) + } else { + this + } + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Any { + defaultDeserializer.deserialize(p, ctxt) + return singletonInstance + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/ObjectSingletonTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/ObjectSingletonTest.kt new file mode 100644 index 000000000..c5b7f90b5 --- /dev/null +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/ObjectSingletonTest.kt @@ -0,0 +1,57 @@ +package com.fasterxml.jackson.module.kotlin.test + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.not +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test + +class ObjectSingletonTest { + + val mapper: ObjectMapper = jacksonObjectMapper() + .configure(SerializationFeature.INDENT_OUTPUT, false) + + object Singleton { + var content = 1 // mutable state + } + + @Test + fun deserializationPreservesSingletonProperty() { + val js = mapper.writeValueAsString(Singleton) + val newSingleton = mapper.readValue(js) + + assertThat(newSingleton, equalTo(Singleton)) + } + + @Test + fun deserializationResetsSingletonObjectState() { + // persist current singleton state + val js = mapper.writeValueAsString(Singleton) + val initial = Singleton.content + + // mutate the in-memory singleton state + val after = initial + 1 + Singleton.content = after + assertThat(Singleton.content, equalTo(after)) + + // read back persisted state resets singleton state + val newSingleton = mapper.readValue(js) + assertThat(newSingleton.content, equalTo(initial)) + assertThat(Singleton.content, equalTo(initial)) + } + + @Test + fun deserializedObjectsBehaveLikeSingletons() { + val js = mapper.writeValueAsString(Singleton) + val newSingleton = mapper.readValue(js) + assertThat(newSingleton.content, equalTo(Singleton.content)) + + newSingleton.content += 1; + + assertThat(Singleton.content, equalTo(newSingleton.content)) + } + +} \ No newline at end of file