115

I am new to Spring Data REST project and I am trying to create my first RESTful service. The task is simple, but I am stuck.

I want to perform CRUD operations on a user data stored in an embedded database using RESTful API.

But I cannot figure out how to make the Spring framework process the birthData as "1999-12-15" and store it as a LocalDate. The @JsonFormat annotation does not help.

At present I get the error:

HTTP/1.1 400 
Content-Type: application/hal+json;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 24 Aug 2017 13:36:51 GMT
Connection: close

{"cause":{"cause":null,"message":"Can not construct instance of java.time.LocalDate: 
no String-argument constructor/factory method to deserialize from String value ('1999-10-10')\n 
at [Source: org.apache.catalina.connector.CoyoteInputStream@4ee2a60e; 
line: 1, column: 65] (through reference chain: ru.zavanton.entities.User[\"birthDate\"])"},
"message":"JSON parse error: Can not construct instance of java.time.LocalDate: 
no String-argument constructor/factory method to deserialize from String value ('1999-10-10'); nested exception is com.fasterxml.jackson.databind.JsonMappingException: 
Can not construct instance of java.time.LocalDate: no String-argument constructor/factory method to deserialize from String value ('1999-10-10')\n 
at [Source: org.apache.catalina.connector.CoyoteInputStream@4ee2a60e; line: 1, column: 65] (through reference chain: ru.zavanton.entities.User[\"birthDate\"])"}

How to make it work, so that client calls like:

curl -i -X POST -H "Content-Type:application/json" -d "{  \"firstName\" : \"John\",  \"lastName\" : \"Johnson\", \"birthDate\" : \"1999-10-10\", \"email\" : \"john@example.com\" }" http://localhost:8080/users

will actually store the entity into the database.

Below is the information about the classes.

The user class:

package ru.zavanton.entities;


import com.fasterxml.jackson.annotation.JsonFormat;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.time.LocalDate;

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    private String firstName;
    private String lastName;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
    private LocalDate birthDate;

    private String email;
    private String password;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public LocalDate getBirthDate() {
        return birthDate;
    }

    public void setBirthDate(LocalDate birthDate) {
        this.birthDate = birthDate;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

The UserRepository class:

package ru.zavanton.repositories;

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import ru.zavanton.entities.User;

@RepositoryRestResource(collectionResourceRel = "users", path = "users")
public interface UserRepository extends PagingAndSortingRepository<User, Long> {

    User findByEmail(@Param("email") String email);

}

Application class:

package ru.zavanton;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {

        SpringApplication.run(Application.class, args);

    }
}
halfer
  • 19,824
  • 17
  • 99
  • 186
zavanton
  • 1,424
  • 2
  • 9
  • 14
  • Possible duplicate of [serialize/deserialize java 8 java.time with Jackson JSON mapper](https://stackoverflow.com/questions/27952472/serialize-deserialize-java-8-java-time-with-jackson-json-mapper) – Juan Carlos Mendoza Aug 24 '17 at 14:24
  • 5
    Add https://github.com/FasterXML/jackson-modules-java8 as dependency to your pom.xml – Cepr0 Aug 24 '17 at 14:46
  • Thanks, @Cepr0! I added the dependency to the pom file and it worked like a charm! – zavanton Aug 24 '17 at 14:57

6 Answers6

142

You need jackson dependency for this serialization and deserialization.

Add this dependency:

Gradle:

compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.4")

Maven:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

After that, You need to tell Jackson ObjectMapper to use JavaTimeModule. To do that, Autowire ObjectMapper in the main class and register JavaTimeModule to it.

import javax.annotation.PostConstruct;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

@SpringBootApplication
public class MockEmployeeApplication {

  @Autowired
  private ObjectMapper objectMapper;

  public static void main(String[] args) {
    SpringApplication.run(MockEmployeeApplication.class, args);

  }

  @PostConstruct
  public void setUp() {
    objectMapper.registerModule(new JavaTimeModule());
  }
}

After that, Your LocalDate and LocalDateTime should be serialized and deserialized correctly.

SanketKD
  • 1,509
  • 2
  • 9
  • 12
  • 3
    I just had to add the gradle dependency to fix it – Tatha Sep 12 '18 at 17:44
  • 8
    That part made it work for me```objectMapper.registerModule(new JavaTimeModule());``` – 0cnLaroche Jan 06 '20 at 15:36
  • There's no need to register the JavaTimeModule. It will be registered automatically if you have the dependency inside the Classpath. – Felipe Desiderati Jan 13 '20 at 22:19
  • 3
    It looks like not registering automatically, I had to register explicitly to work. – Yoga Gowda Apr 16 '20 at 18:33
  • 1
    You can also have the modules auto-discovered as suppose to explicitly registering them. eg: `new ObjectMapper().findAndRegisterModules();` That said, you should not really mix manual and auto registration if you do, only one of the registrations will have effect. [Source: FasterXML jackson-modules-java8](https://github.com/FasterXML/jackson-modules-java8) – DaddyMoe Nov 05 '20 at 11:51
  • Im still getting the error. In my REST endpoint I am doing: `Registration registration = mapper.convertValue(map.get("person"), Registration.class);` What am I doing wrong? – farahm Nov 17 '20 at 22:59
  • I've just registered the `JavaTimeModule` without adding the dependency, and it works! Had some Jackson dependencies though.. – Katia Savina Oct 26 '21 at 16:00
36

Spring Boot 2.2.2 / Gradle:

Gradle (build.gradle):

implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")

Entity (User.class):

LocalDate dateOfBirth;

Code:

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
User user = mapper.readValue(json, User.class);
sparkyspider
  • 13,195
  • 10
  • 89
  • 133
  • Thanks, I tried lot many solution but that are not working, but this one is best. How can i study and get more detail for the functionality of this class? – Dhwanil Patel Apr 28 '20 at 07:18
  • 1
    The yyyy-mm-dd format is standardized under ISO 8601. JSR-310 specifies additions to the Java 8 Date and time API to incorporate ISO8601. The dependency shown above is a patch to add this functionality to Jackson versions prior to 2.85. Version 2.85 and onward support the Java Date and Time API. More info here: https://github.com/FasterXML/jackson-modules-java8/tree/master/datetime – sparkyspider Apr 29 '20 at 08:39
  • In addition to this correct answer note also, you can have the modules auto-discovered as suppose to manually register them. eg: `new ObjectMapper().findAndRegisterModules();` That said, you should not really mix manual and auto registration if you do, only one of the registrations will have effect. [Source: FasterXML jackson-modules-java8](https://github.com/FasterXML/jackson-modules-java8) – DaddyMoe Nov 05 '20 at 11:50
26

I had a similar issue which I solved by making two changes

  1. Added below entry in application.yaml file
spring:
    jackson:
        serialization.write_dates_as_timestamps: false
  1. Add below two annotations to the POJO's LocalDate field
@JsonDeserialize(using = LocalDateDeserializer.class)
@JsonSerialize(using = LocalDateSerializer.class)

Example

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

public class Customer   {
    @JsonDeserialize(using = LocalDateDeserializer.class)
    @JsonSerialize(using = LocalDateSerializer.class)
    protected LocalDate birthdate;
}

Eample request format:

{"birthdate": "2019-11-28"}

Example request format as array

{"birthdate":[2019,11,18]}
xlm
  • 6,854
  • 14
  • 53
  • 55
Mateen
  • 1,631
  • 1
  • 23
  • 27
  • 2
    Thank you so much! You saved my time! I also added this line of code to pojo @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm") – Orkhan Hasanli Nov 23 '20 at 20:20
  • This one looks clean to me. – CN1002 Dec 31 '20 at 12:35
  • FWIW I found that modifying the `application.yaml` was not needed but it was necessary to add dependency `jackson-datatype-jsr310` and also include `@JsonFormat` as suggested – xlm Mar 16 '21 at 05:13
16

As it turns out, one should not forget to include jacson dependency into the pom file. This solved the issue for me:

<dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-parameter-names</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
zavanton
  • 1,424
  • 2
  • 9
  • 14
  • 7
    What if even after adding these dependencies it is not working. Is there any way I can ask Json to ignore this param or something like that? – coretechie Feb 06 '18 at 11:35
8

Well, what I do on every project is a mix of the options above.

First, add the jsr310 dependency:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

Important detail: put this dependency on the top of your depedencies list. I already see a project where the Localdate error persists even with this dependency on the pom.xml. But changing the order of the depedency the error was gone.

On your /src/main/resources/application.yml file, setup the write-dates-as-timestamps property:

spring:
  jackson:
    serialization:
      write-dates-as-timestamps: false

And create a ObjectMapper bean as this:

@Configuration
public class WebConfigurer {

    @Bean
    @Primary
    public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
        ObjectMapper objectMapper = builder.build();
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        return objectMapper;
    }

}

Following this configuration, the conversion always work on Spring Boot 1.5.x without any error.

Bonus: Spring AMQP Queue configuration

Working with Spring AMQP, pay attention if you have a new instance of Jackson2JsonMessageConverter (common thing when creating a SimpleRabbitListenerContainerFactory). You need to pass the ObjectMapper bean to it, like:

Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(objectMapper);

Otherwise, you will receive the same error.

Dherik
  • 17,757
  • 11
  • 115
  • 164
8

I have just wrestled with this for 3 hours. I credit the answer from Dherik (Bonus material about AMQP) for bringing me within striking distance of MY answer, YMMV.

I registered the JavaTimeModule in my object mapper in my SpringBootApplication like this:

@Bean
@Primary
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
    ObjectMapper objectMapper = builder.build();
    objectMapper.registerModule(new JavaTimeModule());
    return objectMapper;
}

However my Instants that were coming over the STOMP connection were still not deserialising. Then I realised I had inadvertantly created a MappingJackson2MessageConverter which creates a second ObjectMapper. So I guess the moral of the story is: Are you sure you have adjusted all your ObjectMappers? In my case I replaced the MappingJackson2MessageConverter.objectMapper with the outer version that has the JavaTimeModule registered, and all is well:

@Autowired
ObjectMapper objectMapper;

@Bean
public WebSocketStompClient webSocketStompClient(WebSocketClient webSocketClient,
        StompSessionHandler stompSessionHandler) {
    WebSocketStompClient webSocketStompClient = new WebSocketStompClient(webSocketClient);
    MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
    converter.setObjectMapper(objectMapper);
    webSocketStompClient.setMessageConverter(converter);
    webSocketStompClient.connect("http://localhost:8080/myapp", stompSessionHandler);
    return webSocketStompClient;
}
spl
  • 510
  • 4
  • 15