I'm implementing a Spring #RestController with a #PostMapping annotated method. I want to allow a HTTP POST using this body:
{"dateTimes":[
"2017-07-19T14:25+02:00",
"2017-08-19T14:25+02:00"
]
}
I have an object used as #RequestBody:
public class TransactionAllowedRequestBody {
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private List<ZonedDateTime> dateTimes;
public List<ZonedDateTime> getDateTimes() {
return dateTimes;
}
public void setDateTimes(List<ZonedDateTime> dateTimes) {
this.dateTimes = dateTimes;
}
}
This is my controller method:
#PostMapping("/transaction-allowed")
public void isTransactionAllowed(#AuthenticationPrincipal CustomUserDetails userDetails,
#RequestBody TransactionAllowedRequestBody requestBody) {
System.out.println("requestBody = " + requestBody);
}
However, when I try this, I get:
Could not read JSON document: Can not construct instance of java.time.ZonedDateTime:
no String-argument constructor/factory method to deserialize from String value ('2017-07-19T14:25+02:00')
If I replace ZonedDateTime with String, it works.
I am using Spring Boot 1.5.3.
Note: Using #DateTimeFormat on a GET request parameter works fine. I tried it with:
#GetMapping("/transaction-allowed")
public void isTransactionAllowed(#AuthenticationPrincipal CustomUserDetails userDetails,
#RequestParam("datetime") #DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) ZonedDateTime dateTime) {
System.out.println("userDetails = " + userDetails);
System.out.println("dateTime = " + dateTime);
}
Seems the issue was that I forget to include the jackson-datatype-jsr310 dependency as Spring Boot will not add it by default:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
You can use #JsonDeserialize and a Deserializer of your own
Like the following one
public class ZonedDateTimeDeserializer extends JsonDeserializer<List<ZonedDateTime>> {
#Override
public List<ZonedDateTime> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ObjectCodec oc = jp.getCodec();
JsonNode array = oc.readTree(jp);
List<ZonedDateTime> dates = new ArrayList<>();
if(array.isArray()) {
for (JsonNode node: array) {
dates.add(ZonedDateTime.parse(node.asText()));
}
}
return dates;
}
}
To use the Deserializer annotation will be like
#JsonDeserialize(using = ZonedDateTimeDeserializer.class)
private List<ZonedDateTime> dateTimes;
Related
In the json of the post request I have several different date formats. I'm having troubled deserializing all at the same time. I've created a configuration class that will handle one or the other just fine. How do I add additional deserializers to handle the other formats?
I don't have access to the POJO to add any annotations there.
Here's an error I get for one of the dates I'm unable to deserialize
JSON parse error: Cannot deserialize value of type java.time.LocalDateTime from String "09/03/2020 10:59:48": Failed to deserialize java.time.LocalDateTime:
#Configuration
public class JacksonConfig {
#Bean
#Primary
public ObjectMapper objectMapper() {
JavaTimeModule module = new JavaTimeModule();
LocalDateTimeDeserializer localDateTimeDeserializer = new LocalDateTimeDeserializer(
DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss"));
module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer);
return Jackson2ObjectMapperBuilder.json().modules(module)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build();
}
}
I was able to resolve my issue by overriding the LocalDateTimeDeserializer's deserialize method. I modified the solution from Configure Jackson to parse multiple date formats
public class MultiDateDeserializer extends LocalDateTimeDeserializer {
public MultiDateDeserializer() {
this(null);
}
public MultiDateDeserializer(DateTimeFormatter formatter) {
super(formatter);
}
private static final long serialVersionUID = 1L;
private static final String[] DATE_FORMATS = new String[] { "yyyy-MM-dd'T'HH:mm:ss", "MM/dd/yyyy HH:mm:ss" };
#Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode node = p.getCodec().readTree(p);
final String date = node.textValue();
for (String DATE_FORMAT : DATE_FORMATS) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT, Locale.ROOT);
try {
return LocalDateTime.parse(date, formatter);
} catch (DateTimeParseException e) {
}
}
throw new ParseException(0,
"Unparseable date: \"" + date + "\". Supported formats: " + Arrays.toString(DATE_FORMATS));
}
}
And then in my JacksonConfig I have...
#Configuration
public class JacksonConfig {
#Bean
#Primary
public ObjectMapper objectMapper() {
JavaTimeModule module = new JavaTimeModule();
MultiDateDeserializer multiDateDeserializer = new MultiDateDeserializer();
module.addDeserializer(LocalDateTime.class, multiDateDeserializer);
return Jackson2ObjectMapperBuilder.json().modules(module)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build();
}
}
I have created my request POJO as follows
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Notification {
#NotNull
private String clientId;
private String userId;
#NotNull
private String requestingService;
#NotNull
private String message;
#NotNull
private String messageType;
when I send request body as follow, it is working fine.
{
"clientId":"9563",
"userId":"5855541",
"requestingService":"cm-dm-service",
"message":"Document Created",
"messageType":"user-msg"
}
But when I sent like below
{
"clientId":"9563",
"userId":true,
"requestingService":"cm-dm-service",
"message":"Document Created",
"messageType":"user-msg"
}
Here is my controller
public ResponseEntity<Status> createNotification(#RequestBody #Valid Notification notification,
BindingResult bindingResult, HttpServletRequest request) throws AppException {
Expected: throw some error
Actual: converting true value for userId to string by jackson.
please let me know is there a way to acheive the Expected behaviour
The jackson NumberDeserializers.BooleanDeserializer is programmed to convert boolean to String.
We can override the deserializer with ours and prevent the conversion and throw an exception instead.
I can give you an example, you try to implement it to your problem statement.
Create a boolean deserialize class
public class MyDeser extends JsonDeserializer {
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonToken t = p.getCurrentToken();
if (t.isBoolean()) {
throw new Exception();
}
else if (t.isNumeric()) {
throw new Exception();
}
else if (t == JsonToken.VALUE_STRING) {
return p.getValueAsString();
}
return null;
}
}
Now inject the deserializer to our application
#SpringBootApplication
#Configuration
public class Application {
#Bean
public SimpleModule injectDeser() {
return new SimpleModule().addDeserializer(String.class, new MyDeser());
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
By default, com.fasterxml.jackson.databind.deser.std.StringDeserializer accepts scalar values. You can implement custom deserialiser and throw exception in this case:
class StrictStringDeserializer extends StringDeserializer {
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonToken token = p.currentToken();
if (token.isScalarValue()) {
ctxt.reportInputMismatch(String.class, "%s is not a `String` value!", token.toString());
return null;
}
return super.deserialize(p, ctxt);
}
}
To register it use SimpleModule:
SimpleModule strictModule = new SimpleModule();
strictModule.addDeserializer(String.class, new StrictStringDeserializer());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(strictModule);
To how to do that in Spring read: Customize the Jackson ObjectMapper.
If you only want to change it for a one field: userId use JsonDeserialize annotation:
#JsonDeserialize(using = StrictStringDeserializer.class)
private String userId;
See also:
Is it possible to use a custom serializer/deserializer for a type by
default in spring?
I have a spring rest service that accepts and gives json output.
#PostMapping(path = "/path", consumes = {"application/json"}, produces = {"application/json"})
public ResponseEntity<RequestData> method(#RequestBody RequestData request){
return request;
}
RequestData contains several dates (XMLGregorianCalendar). I cannot change the type, since it is generated from xsd. To get dates with the original time zones, I used the parameter
spring.jackson.date-format: yyyy-MM-dd'T'HH:mm:ssZ
Request
{
"date1":"2020-02-28T09:26:59+09:00",
"date2":"2020-01-10T12:46:29+04:00",
"date3":"2020-03-15T11:32:43+08:00"
}
From this request, I got an XMLGregorianCalendar with different time zones.
But when sending a response message, the dates are converted to 0 time zone.
Response
{
"date1":"2020-02-28T00:26:59+0000",
"date2":"2020-01-10T08:46:29+0000",
"date3":"2020-03-15T03:32:43+0000"
}
What settings need to be done on jackson to get non-zero time zones in the response? It is necessary that the response time zones returned in the request.
Or maybe jackson does not know how to do this and always converts the date to a single time zone? In that case, which library to use?
Thanks!
Solution
You must create a serializer and deserializer. Then you need to override the existing ObjectMapper.
If only the serializer is overrided, then upon receipt of the data, the time zone will be normalized (reduced to +00:00), therefore it is also necessary to override the deserializer.
Serializer:
public class XMLGCSerializer extends JsonSerializer<XMLGregorianCalendar> {
#Override
public void serialize(XMLGregorianCalendar value,
JsonGenerator gen,
SerializerProvider serializers)
throws IOException {
gen.writeObject(value.toString());
}
}
Deserializer:
public class XMLGCDeserializer extends JsonDeserializer<XMLGregorianCalendar> {
#Override
public XMLGregorianCalendar deserialize(JsonParser parser, DeserializationContext context) throws IOException {
String stringDate = parser.getText();
try {
return DatatypeFactory.newInstance().newXMLGregorianCalendar(stringDate);
} catch (Exception e) {
throw new RuntimeException(e);
//or return null
}
}
}
Override ObjectMapper
#Component
public class JacksonConfig {
private final ObjectMapper objectMapper;
public JacksonConfig() {
objectMapper = new ObjectMapper();
SimpleModule s = new SimpleModule();
s.addSerializer(XMLGregorianCalendar.class, new XMLGCSerializer());
s.addDeserializer(XMLGregorianCalendar.class, new XMLGCDeserializer());
objectMapper.registerModule(s);
}
#Bean
public ObjectMapper getContext() {
return objectMapper;
}
}
You can create a seperate class to handle serialization by yourself. Here is an example:
class XMLGCSerializer extends JsonSerializer<XMLGregorianCalendar> {
#Override
public void serialize(XMLGregorianCalendar value,
JsonGenerator gen,
SerializerProvider serializers)
throws IOException {
gen.writeObject(value.toString());
}
}
Now you just need to annotate your fields in RequestData:
class RequestData{
#JsonSerialize(using = XMLGCSerializer.class)
XMLGregorianCalendar date1;
//...
}
I have Match class and field Date start. My goal is get start as timestamp. I use Spring, AngularJs, and jackson as json converter.
Spring Controller:
#RequestMapping(value = "/web2/getMatch", method =RequestMethod.POST)
public #ResponseBody Match getPicksHistory() {
PickDAO pd = new PickDAO();
return pd.getMatch();
}
On AgularJS controler:
var res = $http.post(urlSer.url+"web2/getMatch");
res.success(function(data, status, headers, config) {
// now returns data.start = "Aug 8, 2015 7:00:00 PM"
// My goal is get as timestamp
});
I assume that by 'timestamp' you mean a numeric timestamp as opposed to a textual representation. You can use a custom ObjectMapper:
#Component
#Primary
public class CustomObjectMapper extends ObjectMapper {
public CustomObjectMapper() {
configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
}
}
I use jackson-databind:2.6.1 and JsonSerializer
#Component
public class JsonDateSerializer extends JsonSerializer<Date>{
#Override
public void serialize(Date date, JsonGenerator gen,
SerializerProvider serializers) throws IOException,
JsonProcessingException {
gen.writeNumber(date.getTime());
}
}
I am trying a simple JSON to de-serialize in to java object. I am however, getting empty String values for java.lang.String property values. In rest of the properties, blank values are converting to null values(which is what I want).
My JSON and related Java class are listed below.
JSON string:
{
"eventId" : 1,
"title" : "sample event",
"location" : ""
}
EventBean class POJO:
public class EventBean {
public Long eventId;
public String title;
public String location;
}
My main class code:
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
try {
File file = new File(JsonTest.class.getClassLoader().getResource("event.txt").getFile());
JsonNode root = mapper.readTree(file);
// find out the applicationId
EventBean e = mapper.treeToValue(root, EventBean.class);
System.out.println("It is " + e.location);
}
I was expecting print "It is null". Instead, I am getting "It is ". Obviously, Jackson is not treating blank String values as NULL while converting to my String object type.
I read somewhere that it is expected. However, this is something I want to avoid for java.lang.String too. Is there a simple way?
Jackson will give you null for other objects, but for String it will give empty String.
But you can use a Custom JsonDeserializer to do this:
class CustomDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException {
JsonNode node = jsonParser.readValueAsTree();
if (node.asText().isEmpty()) {
return null;
}
return node.toString();
}
}
In class you have to use it for location field:
class EventBean {
public Long eventId;
public String title;
#JsonDeserialize(using = CustomDeserializer.class)
public String location;
}
It is possible to define a custom deserializer for the String type, overriding the standard String deserializer:
this.mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(String.class, new StdDeserializer<String>(String.class) {
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String result = StringDeserializer.instance.deserialize(p, ctxt);
if (StringUtils.isEmpty(result)) {
return null;
}
return result;
}
});
mapper.registerModule(module);
This way all String fields will behave the same way.
You might first like to see if there has been any progress on the Github issue requesting this exact feature.
For those using Spring Boot: The answer from jgesser was the most helpful to me, but I spent a while trying to work out the best way to configure it in Spring Boot.
Actually, the documentation says:
Any beans of type com.fasterxml.jackson.databind.Module are
automatically registered with the auto-configured
Jackson2ObjectMapperBuilder and are applied to any ObjectMapper
instances that it creates.
So here's jgesser's answer expanded into something you can copy-paste into a new class in a Spring Boot application
#Configuration
public class EmptyStringAsNullJacksonConfiguration {
#Bean
SimpleModule emptyStringAsNullModule() {
SimpleModule module = new SimpleModule();
module.addDeserializer(
String.class,
new StdDeserializer<String>(String.class) {
#Override
public String deserialize(JsonParser parser, DeserializationContext context)
throws IOException {
String result = StringDeserializer.instance.deserialize(parser, context);
if (StringUtils.isEmpty(result)) {
return null;
}
return result;
}
});
return module;
}
}
I could get this by following configuration.
final ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
it is possible to use JsonCreator annotation. It worked for me
public class Foo {
private String field;
#JsonCreator
public Foo(
#JsonProrerty("field") String field) {
this.field = StringUtils.EMPTY.equals(field) ? null : field ;
}
}