Reputation: 1454
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\" : \"[email protected]\" }" 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);
}
}
Upvotes: 118
Views: 193560
Reputation: 1758
You need to add below code to your pom.xml modules-java8
<!-- Java 8 Date/time -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
Upvotes: 0
Reputation: 1671
I had a similar issue which I solved by making two changes
application.yaml
filespring:
jackson:
serialization.write_dates_as_timestamps: false
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]}
Upvotes: 29
Reputation: 651
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;
}
Upvotes: 8
Reputation: 13529
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);
Upvotes: 38
Reputation: 19120
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.
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.
Upvotes: 8
Reputation: 1531
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.
Upvotes: 143
Reputation: 1454
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>
Upvotes: 16