'How to deserialise a field and tell the difference between the value being null or absent?
When I am deserialising into my class, for some fields, I would like to be able to tell the difference between the value being absent, or null. For example, {"id": 5, "name": null}
should be considered different to {"id": 5}
.
I have come across solutions for kotlinx.serialisation and Rust's serde, but so far, I'm struggling to achieve this in Jackson.
I'll use this class as an example:
data class ResponseJson(
val id: Int,
@JsonDeserialize(using = OptionalPropertyDeserializer::class)
val name: OptionalProperty<String?>
)
The definition of the OptionalProperty field:
sealed class OptionalProperty<out T> {
object Absent : OptionalProperty<Nothing>()
data class Present<T>(val value: T?) : OptionalProperty<T>()
}
I've written a custom deserialiser:
class OptionalPropertyDeserializer :
StdDeserializer<OptionalProperty<*>>(OptionalProperty::class.java),
ContextualDeserializer
{
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): OptionalProperty<*> {
println(p.readValueAs(ctxt.contextualType.rawClass))
return OptionalProperty.Present(p.readValueAs(ctxt.contextualType.rawClass))
}
override fun getNullValue(ctxt: DeserializationContext?) = OptionalProperty.Present(null)
override fun getAbsentValue(ctxt: DeserializationContext?) = OptionalProperty.Absent
override fun createContextual(ctxt: DeserializationContext, property: BeanProperty): JsonDeserializer<*> {
println(property.type.containedType(0))
return ctxt.findContextualValueDeserializer(property.type.containedType(0), property)
}
}
Finally, my ObjectMapper setup:
val messageMapper: ObjectMapper = jacksonObjectMapper()
.disable(ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
.disable(ACCEPT_FLOAT_AS_INT)
.enable(FAIL_ON_NULL_FOR_PRIMITIVES)
.enable(FAIL_ON_NUMBERS_FOR_ENUMS)
.setSerializationInclusion(NON_EMPTY)
.disable(WRITE_DATES_AS_TIMESTAMPS)
Now, I try to deserialise some JSON:
@Test
fun deserialiseOptionalProperty() {
assertEquals(
ResponseJson(5, OptionalProperty.Present("fred")),
messageMapper.readValue(
//language=JSON
"""
{
"id": 5,
"name": "fred"
}
""".trimIndent()
)
)
}
I am getting the following exception:
com.fasterxml.jackson.databind.exc.ValueInstantiationException: Cannot construct instance of `serialisation_experiments.JacksonTests$ResponseJson`, problem: argument type mismatch
at [Source: (String)"{
"id": 5,
"name": "fred"
}"; line: 4, column: 1]
What does "argument type mismatch" mean here? I assume I've done something incorrectly with the custom deserialiser, but what is the correct approach?
Solution 1:[1]
I used an implementation of OptionalProperty
class that has a field to hold the value and a flag to indicate if the value was set. The flag will be set to true whenever the value is changed, using a custom setter. With such a class and the default jacksonObjectMapper()
, I was able to get the deserialization of all scenarios working - name specified, name null and name missing. Below are the classes I ended up with:
OptionalProperty:
class OptionalProperty {
var value: Any? = null
set(value) {
field = value
valueSet = true
}
var valueSet: Boolean = false
}
ResponseJson:
data class ResponseJson (
@JsonDeserialize(using = OptionalPropertyDeserializer::class)
val id: OptionalProperty,
@JsonDeserialize(using = OptionalPropertyDeserializer::class)
val name: OptionalProperty
)
OptionalPropertyDeserializer:
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
class OptionalPropertyDeserializer : JsonDeserializer<OptionalProperty>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): OptionalProperty {
var property = OptionalProperty()
property.value = parser.readValueAs(Any::class.java)
return property
}
override fun getNullValue(context: DeserializationContext): OptionalProperty {
var property = OptionalProperty()
property.value = null
return property
}
override fun getAbsentValue(context: DeserializationContext): OptionalProperty {
return OptionalProperty()
}
}
Tests:
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Test
class ResponseJsonTest {
var objectMapper = jacksonObjectMapper()
@Test
fun `test deserialization - id and name are non-null`() {
var responseJson = objectMapper.readValue(
"""
{
"id": 5,
"name": "test"
}
""".trimIndent(),
ResponseJson::class.java
)
assert(responseJson.id.value == 5)
assert(responseJson.id.valueSet)
assert(responseJson.id.value is Int)
assert(responseJson.name.value == "test")
assert(responseJson.name.valueSet)
assert(responseJson.name.value is String)
}
@Test
fun `test deserialization - name is null`() {
var responseJson = objectMapper.readValue(
"""
{
"id": 5,
"name": null
}
""".trimIndent(),
ResponseJson::class.java
)
assert(responseJson.id.value == 5)
assert(responseJson.id.valueSet)
assert(responseJson.id.value is Int)
assert(responseJson.name.value == null)
assert(responseJson.name.valueSet)
}
@Test
fun `test deserialization - name is absent`() {
var responseJson = objectMapper.readValue(
"""
{
"id": 5
}
""".trimIndent(),
ResponseJson::class.java
)
assert(responseJson.id.value == 5)
assert(responseJson.id.valueSet)
assert(responseJson.id.value is Int)
assert(responseJson.name.value == null)
assert(!responseJson.name.valueSet)
}
@Test
fun `test deserialization - id is null`() {
var responseJson = objectMapper.readValue(
"""
{
"id": null,
"name": "test"
}
""".trimIndent(),
ResponseJson::class.java
)
assert(responseJson.id.value == null)
assert(responseJson.id.valueSet)
assert(responseJson.name.value == "test")
assert(responseJson.name.valueSet)
assert(responseJson.name.value is String)
}
@Test
fun `test deserialization - id is absent`() {
var responseJson = objectMapper.readValue(
"""
{
"name": "test"
}
""".trimIndent(),
ResponseJson::class.java
)
assert(responseJson.id.value == null)
assert(!responseJson.id.valueSet)
assert(responseJson.name.value == "test")
assert(responseJson.name.valueSet)
assert(responseJson.name.value is String)
}
}
The full project with main and test classes can be found in github
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 |