We use a lot of value objects with a single value. For (de-)serialization we use Jackson with the kotlin-module.
A example value object in Kotlin:
data class MyValueObject(val value: String)
or as Java
class MyValueObject {
private String value;
public MyValueObject(String value) { this.value = value; }
public String getValue() { return value; }
}
These value objects have to be serialized and deserialized and should be serialized "value only", eg. "theValue"
instead of "{"value":"theValue"}"
.
I would like to avoid writing custom serializers/deserializers for dozens of value objects.
I know for serialisation @JsonValue
can be used to realize the above.
data class MyValueObject(@JsonValue val value: String)
But the JSON ""theValue"" (the above serialized String-Literal) cannot be deserialized back as MyValueObject
. It sesults in the following exception:
com.fasterxml.jackson.databind.exc.MismatchedInputException:
Cannot construct instance of `[...].MyValueObject` (although at least one Creator exists):
no String-argument constructor/factory method to deserialize from String value ('test')
at [Source: (String)""test""; line: 1, column: 1]
Here is the Unit Test I used:
@Test
fun testSerialize() {
val objectMapper = ObjectMapper()
.registerModule(Jdk8Module())
.registerModule(KotlinModule())
val test = MyValueObject("test")
val json = objectMapper.writeValueAsString(test)
println(json)
objectMapper.readValue<MyValueObject>(json)
}
Is there a simple/generic way to deserialize these like @JsonValue
for serializing?
P.S.: One working solution (thanks to @LppEdd) is:
data class MyValueObject (@JsonValue val value: String) {
companion object {
@JvmStatic
@JsonCreator
fun create(value: String) = MyValueObject(value)
}
}
But this is very verbose. @JsonCreator annotated on the constructor did not work for me (see my comment on the answer of @LppEdd)
Sure, and it's pretty simple.
Just add @JsonCreator
to the constructor.
class MyValueObject {
private String value;
@JsonCreator
public MyValueObject(String value) { this.value = value; }
@JsonValue
public String getValue() { return value; }
}
Or for Kotlin you'd have, I suppose
data class MyValueObject @JsonCreator constructor(@JsonValue val value: String)
When you use @JsonCreator
without prefixing the constructor parameter with
@JsonProperty("fieldName")
you tell Jackson to pass the entire JSON string, which in your case is just a "primitive" value.
Apparently Kotlin doesn't like auto-generated getter/setters pairs.
This works for example
class MyValueObject @JsonCreator constructor(private val value: String) {
@JsonValue
fun getValue() = value
}
Also, after a bit of debugging, I found out the problem arises here
@Override
public Boolean hasAsValue(Annotated a) {
JsonValue ann = _findAnnotation(a, JsonValue.class);
if (ann == null) {
return null;
}
return ann.value();
}
At JacksonAnnotationIntrospector
.
I don't know why but Jackson still look for a @JsonValue
annotation on a public field or on a public getter. Kotlin places the annotation on the private field, so Jackson cannot find it.
The solution is
data class MyValueObject @JsonCreator constructor(@JvmField @JsonValue val value: String)
or even better
data class MyValueObject @JsonCreator constructor(@get:JsonValue val value: String)
As Marc von Renteln wrote in the comments, you can also omit @JsonCreator
data class MyValueObject(@get:JsonValue val value: String)
Which seems undocumented, however. If someone can point out where this behavior is described, it would be awesome!