ksl
ksl

Reputation: 4719

Error deserialising LocalDateTime in POST request

I have a class which contains two LocalDateTime members:

public class Foo
{
    private final LocalDateTime start;
    private final LocalDateTime end;

    public Foo(LocalDateTime start, LocalDateTime end)
    {
        this.start = start;
        this.end = end;
    }
}

I have a Spring Boot controller to handle a POST request:

@PostMapping(value = "/my-resource")
public ResponseEntity<?> bar(@RequestBody Foo foo)
{
   return ResponseEntity.ok().build();
}

If I send a POST request as follows:

curl -X POST \
  http://localhost:8080/my-resource \
  -H 'Content-Type: application/json' \
  -d '{
    "start":[2016, 1, 1, 10, 24],
    "end":[2016, 1, 1, 10, 24]
}
'

the following error is returned:

"message": "Type definition error: [simple type, class Foo]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `Foo` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)\n at [Source: (PushbackInputStream); line: 2, column: 2]"

After some digging around I added a default ctor to Foo and that resolved the error. I also had to remove final from the members.

I don't understand why this solution works. Can someone explain this?

Upvotes: 1

Views: 128

Answers (2)

cassiomolin
cassiomolin

Reputation: 131137

Simply annotate the constructor with @JsonCreator:

@JsonCreator
public Foo(LocalDateTime start, LocalDateTime end) {
    this.start = start;
    this.end = end;
}

Once your dates use a very particular format, you'll need to write a custom deserializer to handle them.


If you stick to ISO 8601, you can rely on the JavaTimeModule: It will provide you with a set of serializers and deserializers for JSR-310 datatypes.

See details here.

Upvotes: 1

user10527814
user10527814

Reputation:

Jackson uses reflection to instantiate the Foo class and this mechanism requires the existence of a default constructor.

After instantiating the Foo class, using reflection again, Jackson will set the field values of that instance - currently all field values are null. Since you declared those fields final it fails to modify them.

Here is an example:

import java.lang.reflect.Field;

public class Reflection {
    public static void main(String[] args) {
        try {
            Class<?> clazz = Child.class;
            Object child = clazz.newInstance();
            Field f1 = child.getClass().getDeclaredField("age");

            // Because 'age' field is private
            // we make it accesible
            f1.setAccessible(true);

            // Set the desired value
            f1.set(child, 10);
            System.out.println("Child: " + child);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class Child {
    private int age;

    @Override
    public String toString() {
        return "Child [age=" + age + "]";
    }

}

The Jackson's serialization/deserialization mechanism is very flexible. There are lots of annotations you can set and configuration you can make.

Here is another example:

public class Jackson {
    public static void main(String[] args) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            String jsonAsString = "{\"age\":10}";
            Child child = mapper.readValue(jsonAsString, Child.class);
            System.out.println(child);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

The above example doesn't work because age field doesn't have accessor methods. To overcome this, we can add:

mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);

and the above example will be working now.

Very good documentation about Jackson can be found here as @JB Nizet pointed out in a comment.

Upvotes: 0

Related Questions