

Cannot deserialize wrapper for simple types if their value property is public in FasterXML jackson-module-kotlin
Explanation of the problem
When using single-property wrapper classes, such as BigDecimal or Double, to deserialize JSON values, the property must be declared private, otherwise an exception will be thrown. This issue occurs when using Jackson library with Kotlin.
The following code block demonstrates the issue:
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import java.math.BigDecimal
data class DtoWithPublicProperty(
val foo: WrapperWithPublicValue
)
data class WrapperWithPublicValue(val value: BigDecimal)
val objectMapper = jacksonObjectMapper()
val json = """{"foo":42.123}"""
objectMapper.readValue<DtoWithPublicProperty>(json)
This code will throw an exception like:
com.fasterxml.jackson.databind.exc.MismatchedInputException:
Cannot construct instance of [..] WrapperWithPublicValue
(although at least one Creator exists):
no double/Double-argument constructor/factory method to deserialize
from Number value (42.123)
at [Source: (String)"{"foo":42.123}"; line: 1, column: 8]
Other details:
- The issue occurs with Kotlin version 1.4.31 and 1.3.x, and Jackson library version 2.12.2 and 2.11.x.
- Other solutions, such as adding a JsonCreator or JsonIgnore, or changing the data type, or using simple classes instead of data classes, did not resolve the issue.
- A sample project with tests to reproduce the issue is available at https://github.com/slu-it/bug-jackson-kotlin-wrapper-with-public-param/blob/main/src/test/kotlin/example/IssueTests.kt.
Troubleshooting with the Lightrun Developer Observability Platform
Getting a sense of what’s actually happening inside a live application is a frustrating experience, one that relies mostly on querying and observing whatever logs were written during development.
Lightrun is a Developer Observability Platform, allowing developers to add telemetry to live applications in real-time, on-demand, and right from the IDE.
- Instantly add logs to, set metrics in, and take snapshots of live applications
- Insights delivered straight to your IDE or CLI
- Works where you do: dev, QA, staging, CI/CD, and production
Start for free today
Problem solution for Cannot deserialize wrapper for simple types if their value property is public in FasterXML jackson-module-kotlin
To solve this issue with FasterXML jackson-module-kotlin, you can follow these steps:
- Use a constructor or factory method to create the wrapper object instead of using the public value property.
- Annotate the value property with
@JsonValue
which will tell Jackson to use it as the single representation of the object during serialization and deserialization. - If you cannot change the code of the class, you can write a custom serializer/deserializer for the class.
Example with @JsonValue:
data class Wrapper(val value: String) {
@JsonValue
fun getValue() = value
}
Example with custom serializer/deserializer:
class WrapperSerializer : JsonSerializer<Wrapper> {
override fun serialize(wrapper: Wrapper, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeString(wrapper.value)
}
}
class WrapperDeserializer : JsonDeserializer<Wrapper> {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Wrapper {
return Wrapper(p.text)
}
}
object WrapperModule : SimpleModule() {
init {
addSerializer(Wrapper::class.java, WrapperSerializer())
addDeserializer(Wrapper::class.java, WrapperDeserializer())
}
}
And finally register the module with ObjectMapper:
val mapper = ObjectMapper().registerModule(WrapperModule)
Other popular problems with jackson-module-kotlin
Problem: Serializing Null Values
By default, FasterXML Jackson omits properties with null values during serialization. This can lead to unexpected behavior, especially when working with REST APIs.
Solution:
To force Jackson to include null values in serialized JSON, you can set the SerializationFeature.WRITE_NULL_MAP_VALUES
feature to true
on the ObjectMapper
used for serialization.
val mapper = ObjectMapper().apply {
enable(SerializationFeature.WRITE_NULL_MAP_VALUES)
}
Problem: Handling Polymorphic Types
When working with polymorphic types (i.e. classes that have multiple subclasses), you need to specify how Jackson should determine the correct type to use during serialization and deserialization.
Solution:
To handle polymorphic types with Jackson, you can use the @JsonTypeInfo
and @JsonSubTypes
annotations to specify the type information to include in the serialized JSON, and how to map the type information to the corresponding class.
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes(
JsonSubTypes.Type(value = Square::class, name = "square"),
JsonSubTypes.Type(value = Circle::class, name = "circle")
)
sealed class Shape
data class Square(val sideLength: Double) : Shape()
data class Circle(val radius: Double) : Shape()
Problem: Handling Dates and Times
By default, FasterXML Jackson uses the default java.util.Date
type to represent dates and times, which can lead to issues with time zones and format.
Solution:
To handle dates and times in a more robust manner, you can use the java.time
package introduced in Java 8 and configure Jackson to use these classes for serialization and deserialization.
You can use the JSR310Module
from the jackson-datatype-jsr310 module to register the Java 8 date and time classes with the ObjectMapper
:
val mapper = ObjectMapper().apply {
registerModule(JavaTimeModule())
}
And then, annotate the date and time properties in your class with the appropriate Jackson annotations, such as @JsonFormat
to specify the format of the serialized string:
data class Event(
@get:JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
val timestamp: LocalDateTime
)
A brief introduction to jackson-module-kotlin
FasterXML jackson-module-kotlin is a Java library for processing JSON data. It is a module for FasterXML Jackson, a widely used and high-performance JSON processing library. The jackson-module-kotlin module provides specific support for the Kotlin programming language, making it easy to serialize and deserialize Kotlin objects to and from JSON.
FasterXML jackson-module-kotlin provides a variety of features to make it easy to work with JSON data in a Kotlin environment. For example, it supports the serialization and deserialization of Nullable types, using the ?
operator in Kotlin. It also provides support for the serialization and deserialization of data classes, making it easy to serialize and deserialize complex data structures. Additionally, the library provides support for custom serialization and deserialization logic, so you can define custom behavior for serializing and deserializing specific types or properties.
Most popular use cases for jackson-module-kotlin
- JSON Processing: FasterXML jackson-module-kotlin can be used to parse and generate JSON data. It provides a simple and efficient API for converting between JSON data and Kotlin objects.
val mapper = ObjectMapper()
val jsonString = """
{
"name": "John Doe",
"age": 35
}
"""
val user = mapper.readValue(jsonString, User::class.java)
- Data Binding: FasterXML jackson-module-kotlin can be used to bind JSON data to Kotlin objects and vice versa. The library provides support for a wide range of data types, including nullables, arrays, and complex data structures.
data class User(val name: String, val age: Int)
val mapper = ObjectMapper()
val user = User("John Doe", 35)
val json = mapper.writeValueAsString(user)
- Custom Serialization and Deserialization: FasterXML jackson-module-kotlin can be used to customize the serialization and deserialization process for specific types and properties. This allows you to define custom behavior for serializing and deserializing specific types or properties, for example, to handle custom date formats or to map JSON properties to different Kotlin properties.
data class User(
@get:JsonProperty("full_name") val name: String,
val age: Int
)
It’s Really not that Complicated.
You can actually understand what’s going on inside your live applications.