How to implement Java 8 LocalTimeDeserializer? - java

I have been looking for implementing a custom deserializer using LocalTimeDeserializer class of Jackson API.
However, I am getting below error while actually deserializing properties using this class.
com.fasterxml.jackson.databind.JsonMappingException: Class com.dspim.api.common.LocalTimeWithStringDeserializer has no default (no arg) constructor
I am using the custom implementation as below for deserializing inside the bean class.
#JsonProperty
#JsonDeserializer(using=LocalTimeWithStringDeserializer.class)
private LocalTime packaging_time; //It will hold value for time i.e. 13:24 (01:24 PM).
I have implemented the deserializer class as follows.
package com.testapp.test;
import java.io.IOException;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
public class LocalTimeWithStringDeserializer extends com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer{
public LocalTimeWithStringDeserializer(DateTimeFormatter formatter) {
super(formatter);
}
private static final long serialVersionUID = 1L;
#Override
public LocalTime deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
return LocalTime.parse(jp.getText());
}
}
The default constructor (with no arguments) in the parent class is private, thus I cannot add a default constructor (with no arguments) to my class implementation as I get compile time error.
Please suggest a solution for this issue.
Please Note: I have two different projects (having dependency on each other added to the classpath) of a client in which I cannot use the built in Jackson deserializer due to dependency version conflicts, that's why I have been compelled to use custom deserializer.

If the parent class has a private no-args constructor, that does not prohibit you from having a no-args constructor yourself:
public class LocalTimeWithStringDeserializer extends com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer{
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd");
public LocalTimeWithStringDeserializer() {
super(formatter);
}
// ...
}

Maybe you don't need your own deserializer. All you need to do is to anotate your LocalTime member with the following anotation:
static class Entity {
LocalTime time;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm:ss")
public LocalTime getTime() {
return time;
}
}
This should work. See the answer to this question: Spring Data JPA - ZonedDateTime format for json serialization

I use LocalTime deserializer in my project and link it for the JSOG/JSON to deserialize when API layer gets called. I declare it using an annotation in the model layer above the field i want to deserialize. I use jackson.databind.JsonDeserializer and it works well for me`.
Here is the code I use:
import java.io.IOException;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
public class CustomJacksonLocalTimeDeserializer extends JsonDeserializer<LocalTime> {
#Override
public LocalTime deserialize(JsonParser jsonparser, DeserializationContext context)
throws IOException, JsonProcessingException {
String dateAsString = jsonparser.getText();
try {
return LocalTime.parse(dateAsString, DateTimeFormatter.ISO_LOCAL_TIME);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
You also need to annotate it on the field as follows:
#JsonDeserialize(using = CustomJacksonLocalTimeDeserializer.class)
#JsonSerialize(using = CustomJacksonLocalTimeSerializer.class)
#Column(columnDefinition = "TIMESTAMP")
private LocalTime origHardScheduledTime;
I know, you did not ask for it, but for the completion's sake, a serializer would also look similar.
import java.io.IOException;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
public class CustomJacksonLocalTimeSerializer extends JsonSerializer<LocalTime> {
#Override
public void serialize(LocalTime date, JsonGenerator jsonGenerator, SerializerProvider serializers)
throws IOException, JsonProcessingException {
jsonGenerator.writeString(date.format(DateTimeFormatter.ISO_LOCAL_TIME));
}
}

Related

How to not write Option.None with jackson objectMapper (and read it)?

I use jackson ObjectMapper to serialize and deserialize some data of mine, which have fields of javaslang Option type. I use JavaslangModule (and Jdk8Module). And when it write the json, Option.None value fields are written as null.
To reduce the json size and provide some simple backward compatibility when later adding new fields, what I want is that:
fields with Option.None value are simply not written,
missing json fields that correspond to data model of Option type, be set to Option.None upon reading
=> Is that possible, and how?
Note:
I think that not-writing/removing null json fields would solve (1). Is it possible? And then, would reading it works (i.e. if model field with Option value is missing in the json, set it None?
Luckily there is a much simpler solution.
1) In your ObjectMapper configuration, set serialization inclusion to only include non absent field:
#Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModules(vavr());
objectMapper.setSerializationInclusion(NON_ABSENT);
return objectMapper;
}
2) Set the default value of your optional fields to Option.none:
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Foo {
private Option<String> bar = Option.none(); // If the JSON field is null or not present, the field will be initialized with none
}
That's it!
And the even better news is that it works for all Iterables, not just for Option. In particular it also works for Vavr List type!
I found a solution that works with immuatble (lombok #Value) models:
add a filter on all Object using mixIn that doesn't write Option.None (see "the solution" below)
my existing ObjectMapper (with JavaslangModule) is already setting None to Option field when the corresponding json entry is missing
The code
import static org.assertj.core.api.Assertions.assertThat;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import javaslang.control.Option;
import javaslang.jackson.datatype.JavaslangModule;
import lombok.AllArgsConstructor;
import lombok.Value;
import org.junit.Test;
import java.io.IOException;
import java.lang.reflect.Field;
public class JsonModelAndSerialization {
// Write to Json
// =============
private static ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module())
.registerModule(new JavaslangModule())
// not required but provide forward compatibility on new field
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
static String write(Object data) throws JsonProcessingException {
SimpleBeanPropertyFilter filter = new NoneOptionPropertyFilter();
objectMapper.addMixIn(Object.class, NoneOptionFilter.class);
final SimpleFilterProvider filters = new SimpleFilterProvider().setDefaultFilter(filter);
ObjectWriter writer = objectMapper.writer(filters);
return writer.writeValueAsString(data);
}
// Filter classes
// ==============
#JsonFilter("Filter None")
private static class NoneOptionFilter {}
private static class NoneOptionPropertyFilter extends SimpleBeanPropertyFilter {
#Override
public void serializeAsField(
Object pojo, JsonGenerator jgen,
SerializerProvider provider, PropertyWriter writer) throws Exception{
Field field = pojo.getClass().getDeclaredField(writer.getName());
if(field.getType().equals(Option.class)){
field.setAccessible(true);
Option<?> value = (Option<?>) field.get(pojo);
if(value.isEmpty()) return;
}
super.serializeAsField(pojo, jgen, provider, writer);
}
}
// Usage example
// =============
// **important note**
// For #Value deserialization, a lombok config file should be added
// in the source folder of the model class definition
// with content:
// lombok.anyConstructor.addConstructorProperties = true
#Value
#AllArgsConstructor(onConstructor_={#JsonCreator})
public static class StringInt {
private int intValue;
private Option<String> stringValue;
}
#Value
#AllArgsConstructor(onConstructor_={#JsonCreator})
public static class StringIntPair {
private StringInt item1;
private StringInt item2;
}
#Test
public void readWriteMyClass() throws IOException {
StringIntPair myClass = new StringIntPair(
new StringInt(6 * 9, Option.some("foo")),
new StringInt( 42, Option.none()));
String json = write(myClass);
// {"item1":{"intValue":54,"stringValue":"foo"},"item2":{"intValue":42}}
StringIntPair myClass2 = objectMapper.readValue(json, StringIntPair.class);
assertThat(myClass2).isEqualTo(myClass);
}
}
The advantages:
reduce size of json when having Option.None (thus adding Option fields in the model doesn't cost size when not used)
it provides backward reading compatibility when later adding field with Option type in the model (which will default to None)
The disadvantage:
It is not possible to differentiate correct data with None field value and incorrect data where the field has erroneously been forgotten. I think this is quite acceptable.

Apply a simple string to string transformation during deserialization

Is there a simple to way to apply a custom logic transformation on the value of a specific key during bean deserialization ?
Concrete example, i receive the following json:
{password: "1234"}
and want a special hash function applied to the password value when deserializing :
User [password: "6265b22b66502d70d5f004f08238ac3c"]
I know i could use a setter User.setPassword() and apply the hash transformation here but the transformation need to make use of "Service" classes which are not available in the context of an Entity (bad use of dependency injection..). This transformation must be made outside of the entity code.
Using a custom Deserializer for User class seems to be overkill for just one attribute too.
Use the annotation to define a custom serializer/deserializer for the bean propery.
Here is an example of a bean where you define your custom serializer/deserializer classes:
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
public class User {
#JacksonXmlProperty
private String login;
#JacksonXmlProperty
#JsonSerialize(using=your.class.package.PasswordSerializer.class)
#JsonDeserialize(using=your.class.package.PasswordDeserializer.class)
private String password;
// ...
}
And here the custo, serializer example:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
import java.io.IOException;
public class PasswordSerializer extends JsonSerializer<String> {
#Override
public void serialize(String s, JsonGenerator jg, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
// do your staff here.
}
}
You just need to implement an interface, and you could do in the proper package.
The deserializer is similar.

Deserializing cyclic JSON with only one class involved with Jackson

Is it possible to deserialize the following class with Jackson?
So the original version of the question wasn't entirely accurate. Here's a minimal example to reproduce the problem.
import java.io.IOException;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import com.fasterxml.jackson.databind.ObjectMapper;
#JsonIdentityInfo(
generator = ObjectIdGenerators.IntSequenceGenerator.class,
property = "id")
public class Thing {
public Thing thing;
#JsonCreator
public Thing(#JsonProperty("thing") Thing thing) {
this.thing = thing;
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Thing cyclic = new Thing(null);
cyclic.thing = cyclic;
String serialised = mapper.writeValueAsString(cyclic);
System.out.println(serialised);
Thing deserialised = mapper.readerFor(Thing.class).readValue(serialised);
System.out.println(deserialised.thing == deserialised);
}
}
This causes the unresolved forward reference exception. The issue seems to be that Jackson is told to use the annotated constructor, but it can't due to the cyclic dependency.
The solution is to add a default constructor, and remove the #JsonProperty and #JsonCreator annotations.

Duplicate values in the output of ObjectMapper.writeValueAsString

I am using Jackson ObjectMapper to (de)serialize a class with polymorphic nested class. The deserialization of JSON to the class is working fine but when I try to serialize the class to JSON using writeValueAsString function I observe duplicate values in the output
public class Movie {
private String movieName;
#JsonTypeInfo(use=Id.NAME,include=As.EXTERNAL_PROPERTY,property="movieName")
#JsonSubTypes({#JsonSubTypes.Type(value = StarWarsParams.class, name = "starwars")})
private MovieParams movieParams;
/* Getters and setters follow */
}
/* Empty class */
public class MovieParams {
}
public class StarWarsParams extends MovieParams{
private String characterName;
#JsonTypeInfo(use=Id.NAME,include=As.EXTERNAL_PROPERTY,property="characterName")
#JsonSubTypes({#JsonSubTypes.Type(value = SithParameters.class, name = "Darth Vader")})
private CharacterParams characterParams;
/* Getters and setters follow */
}
/* Empty class */
public class CharacterParams {
}
public class SithParameters extends CharacterParams {
private boolean canShootLightning;
}
The code snippet where the conversion is done as follows:
Movie movie = new Movie();
movie.setMovieName("starwars");
StarWarsParams starWarsParams = new StarWarsParams();
starWarsParams.setCharacterName("Darth Vader");
SithParameters sithParameters = new SithParameters();
sithParameters.setCanShootLightning(false);
starWarsParams.setCharacterParams(sithParameters);
movie.setMovieParams(starWarsParams);
ObjectMapper mapper = new ObjectMapper();
String jsonStringSample = mapper.writeValueAsString(movie);
System.out.println(jsonStringSample);
The output, in which movieName and characterName have duplicates are as follows:
{"movieName":"starwars","movieParams":{"characterName":"Darth Vader","characterParams":{"canShootLightning":false},"characterName":"Darth Vader"},"movieName":"starwars"}
This problem appears with older versions of Jackson e.g. 1.9.2 but not the latest ones from com.fasterxml. Jackson identifies 2 fields one from the #JsonTypeInfo annotation and one from the getter. Two solutions :
Use a more recent version of Jackson from com.fasterxml
Move the #JsonTypeInfo annotation over the getter instead of over the field e.g.
#JsonTypeInfo(use = Id.NAME, include = As.EXTERNAL_PROPERTY, property = "characterName")
public String getCharacterName() {
return characterName;
}
Customized JSON Object using Serialization is Very Simple.
I have wrote a class in my project to get Serialized JSONObject. i am giving u a Idea to how to Implement this in Project.
Application (POJO Class)
import java.io.Serializable;
import java.util.List;
import org.webservice.business.serializer.ApplicationSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
#JsonSerialize(using=ApplicationSerializer.class)
public class Application implements Serializable {
private static final long serialVersionUID = 1L;
private double amount;
private String businessType;
private String currency;
private int duration;
}
Now ApplicationSerializer class that contains the Customization using Serialization Logic................
package org.webservice.business.serializer;
import java.io.IOException;
import org.webservice.business.dto.Application;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
public class ApplicationSerializer extends JsonSerializer<Application> {
#Override
public void serialize(Application prm_objObjectToSerialize, JsonGenerator prm_objJsonGenerator, SerializerProvider prm_objSerializerProvider) throws IOException, JsonProcessingException {
if (null == prm_objObjectToSerialize) {
} else {
try {
prm_objJsonGenerator.writeStartObject();
prm_objJsonGenerator.writeNumberField("amount", prm_objObjectToSerialize.getAmount());
prm_objJsonGenerator.writeNumberField("duration", prm_objObjectToSerialize.getDuration());
prm_objJsonGenerator.writeStringField("businesstype", prm_objObjectToSerialize.getBusinessType());
prm_objJsonGenerator.writeStringField("currency", prm_objObjectToSerialize.getCurrency());
} catch (Exception v_exException) {
v_exException.printStackTrace()
} finally {
prm_objJsonGenerator.writeEndObject();
}
}
}

Array Serialize Gives Qualified Class name using Jackson

I am using JSON Serialization. Here is my code.
I need to change the qualified class name using Annotation. I don't have to use Map or another class. Name should be picked from Annotation.
package com.test;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
#JsonTypeName("Product")
#JsonRootName("Product")
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="type")
public class ProductDTO {
private String name;
private String description;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
Test Class:-
package com.test;
import java.io.IOException;
import java.util.ArrayList;
import org.junit.Test;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class ProductDTOTestCase {
#Test
public void testPersistAndFindById() throws JsonGenerationException, JsonMappingException, IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
//mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE, JsonTypeInfo.As.WRAPPER_ARRAY);
ProductDTO productDTO = new ProductDTO();
productDTO.setDescription("Product 4 - Test");
ArrayList<ProductDTO> arrayList = new ArrayList<ProductDTO>();
arrayList.add(productDTO);
// Do not change this line
String writeValueAsString = mapper.writeValueAsString(arrayList);
System.out.println(writeValueAsString);
// /Assert.assertTrue(writeValueAsString.contentEquals("Entity"));
}
}
It gives me:-
[["com.test.ProductDTO",{"name":null,"description":"Product 4 - Test"}]]
But I want
[["Product",{"name":null,"description":"Product 4 - Test"}]]
Problem is that you serialize basically a generic ArrayList (the Class of your ArrayList, since no generic info is available).
As a consequence, our basic List.class has simply no #JsonTypeInfo annotation or whatsoever about the items contained, no type information is available, that's why you get the simple name of your class.
Solutions:
Use custom sub-class like "class MyList extends ArrayList { }" --> it will NOT suffer from this kind of type erasure (generic type info is hidden, but retained) and will work as expected, OR
Use ObjectWriter and specify full generic type in your serialization: mapper.writerForType(listType).writeValueAsString() (listType you can construct using TypeFactory or using TypeReference)

Categories

Resources