Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a generic way to deserialize single-valued value objects (without custom Deserializer) in Jackson

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)

like image 545
Marc von Renteln Avatar asked Apr 03 '19 10:04

Marc von Renteln


People also ask

How does Jackson deserialization work?

Jackson uses default (no argument) constructor to create object and then sets value using setters. so you only need @NoArgsConstructor and @Setter.

What is Jackson serialization and deserialization?

Jackson is a powerful and efficient Java library that handles the serialization and deserialization of Java objects and their JSON representations.

Does Jackson use Java serialization?

Note that Jackson does not use java.


1 Answers

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!

like image 165
LppEdd Avatar answered Nov 15 '22 00:11

LppEdd