Apply a simple string to string transformation during deserialization - java

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.

Related

Order of the variables when using com.fasterxml.jackson.databind.ObjectWriter

I'm having a class which used to convert POJO to json string and compare it with another json string(read from logs). Recently we added 2 new boolean parameters to json and POJO and now the order of those 2 newly added variables is getting vary time to time.
I know by adding #JsonPropertyOrder(alphabetic = true) or #JsonPropertyOrder({ "att1", "att2", "att3" }) annotations, we can ensure the order. But I just want to know, how we got the same output earlier(with same order. Note: we had that code since 5years back)
Please find the sample code :
package com.....api;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
public class TestClass {
public String writeAsJson(Object object) throws JsonProcessingException {
JsonMapper.Builder jsonMapperBuilder = JsonMapper.builder();
jsonMapperBuilder.addModule(new Jdk8Module())
.addModule((new JavaTimeModule()))
.addModule((new SimpleModule()))
.propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
.disable(new SerializationFeature[]{SerializationFeature.WRITE_DATES_AS_TIMESTAMPS})
.enable(new MapperFeature[]{MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS})
.disable(new MapperFeature[]{MapperFeature.DEFAULT_VIEW_INCLUSION})
.disable(new DeserializationFeature[]{DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES});
ObjectMapper objectMapper = jsonMapperBuilder.build();
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
}
}
As per I checked, we can not ensure the order of the variables in the POJO when it serializing and deserializing json<--> POJO. But I'm curious how we got the same output for such a long period and suddenly how it fail after adding 2 new boolean variables.

How to implement Java 8 LocalTimeDeserializer?

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));
}
}

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.

Java - how to deserialize into a logically identical class?

I don't want to go into too much detail, so I'll try to boil it down as simple as I can. We've got an app (Java, Spring Boot) that generates some information which we then serialize and store in a database. To serialize it, we use ObjectMapper:
final ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.enableDefaultTyping(); // default to using DefaultTyping.OBJECT_AND_NON_CONCRETE
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
manifestJson = mapper.writeValueAsString(manifest);
And the JSON is then stored in the database. Yippy yay. manifest is a complex object that contains a number of other objects and information that's used to created a final product. The problem is when we go to try to use that info to generate a new object. The FQN of the original class is stored with the information (makes sense). But the read back of the information is taking place in a different location/application. So while the original namespace that was stored with the json was type a.b.c.d.e.f.class1, we're now trying to read it back into class a.b.c.g.h.i.f.class1 ... logically class1 is the same in both namespacesses, in fact it was copied directly from one to the other. The only difference is the intervening package names.
Naturally, the when we then try to deserialize the JSON, it throws an error about not finding the type, the original type is in the original project/applicaiton and the "new" type is in the current project. We could reference the original app from the secondary app, but for functional reasons, we're trying to maintain decoupling between the two.
So the question is, how to get a serialized an object from one project and then deserialize it into another class that is logically identical?
By default, when typing is not used you can generate a JSON payload which you can later deserialise to any model which fits this JSON. Or even to Map or/and List objects if you do not want to create a model at all.
In your case, when JSON payloads are used by two different apps with two different models, you should not attach class info because it does not provide any extra information for another model. One JSON can be deserialised on many ways and used for different purposes so linking it with one model causes problems like you noticed in your example. So, if it is possible disable typing and provide explicitly all informations are needed later to recreate the same object from a JSON.
If it is not possible, just provide your custom TypeIdResolver and map one class from one model to similar class in another model. Simple example which shows an idea:
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DatabindContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.impl.ClassNameIdResolver;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.SimpleType;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JsonApp {
public static void main(String[] args) throws Exception {
SimpleModule model2Module = new SimpleModule("Model2");
model2Module.setMixInAnnotation(PojoA2.class, CustomJsonTypeIdResolverMixIn.class);
model2Module.setMixInAnnotation(PojoB2.class, CustomJsonTypeIdResolverMixIn.class);
model2Module.setMixInAnnotation(PojoC2.class, CustomJsonTypeIdResolverMixIn.class);
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
mapper.registerModule(model2Module);
String json = mapper.writeValueAsString(new PojoA());
System.out.println(json);
System.out.println(mapper.readValue(json, PojoA.class));
System.out.println(mapper.readValue(json, PojoA2.class));
}
}
#JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM)
#JsonTypeIdResolver(value = Model2ClassNameIdResolver.class)
interface CustomJsonTypeIdResolverMixIn { }
class Model2ClassNameIdResolver extends ClassNameIdResolver {
private final Map<String, JavaType> types = new HashMap<>();
public Model2ClassNameIdResolver() {
super(null, null);
types.put("com.celoxity.PojoA", SimpleType.constructUnsafe(PojoA2.class));
types.put("com.celoxity.PojoB", SimpleType.constructUnsafe(PojoB2.class));
types.put("com.celoxity.PojoC", SimpleType.constructUnsafe(PojoC2.class));
}
#Override
public JavaType typeFromId(DatabindContext context, String id) throws IOException {
JavaType javaType = types.get(id);
if (javaType != null) {
return javaType;
}
return super.typeFromId(context, id);
}
}
class PojoA2 {
private PojoB2 b;
}
class PojoB2 {
private List<PojoC2> c;
}
class PojoC2 {
private String s;
private int i;
}
class PojoA {
private PojoB b;
}
class PojoB {
private List<PojoC> c;
}
class PojoC {
private String s;
private int i;
}
Two models: PojoA, PojoB, PojoC and PojoA2, PojoB2, PojoC2 have the same structure and have only different names. PojoA is the same as PojoA2, etc.
Above code prints:
["com.celoxity.PojoA",{"b":["com.celoxity.PojoB",{"c":["java.util.ArrayList",[["com.celoxity.PojoC",{"s":"Vika","i":22}]]]}]}]
and later:
PojoA{b=PojoB{c=[PojoC{s='Vika', i=22}]}}
PojoA2{b=PojoB2{c=[PojoC2{s='Vika', i=22}]}}

Jackson 3rd Party Class With No Default Constructor

I'm trying to use Jackson to read/write my POJOs to/from Json. As of right now, I've got it configured and working for my classes, except for a 3rd party class. When trying to read in the Json I get the error:
org.codehaus.jackson.map.JsonMappingException: No suitable constructor found for type
After a few quick google searches, it appears that my class needs either a default constructor or to override the default constructor with annotations. Unfortunately, the class in which this is failing is from a 3rd party library and that class does not have a default constructor and I obviously cannot over-write the code.
So my question is, is there anything I can do about this or am I just out of luck?
Thanks.
You could make use of Jackson's Mix-Ins feature, coupled with the Creator feature. The Mix-Ins feature alleviates the need to annotate the original third-party code, and the Creator feature provides a mechanism for custom instance creation.
For yet more customization, it's not too involved to write a custom deserializer.
One approach is to implement a custom JsonDeserializer to create the instance, annotating the fields of the type with #JsonDeserialize. One advantage of this approach over e.g. mixins is that it does not require modifying the ObjectMapper.
The StdNodeBasedDeserializer class allows mapping from a JsonNode representing the value to the desired type.
Type lacking a constructor
public class ThirdPartyType {
private String stringProperty;
private int intProperty;
private Object[] arrayProperty;
public ThirdPartyType(String a, int b, Object[] c) {
this.stringProperty = a;
this.intProperty = b;
this.arrayProperty = c;
}
// Getters and setters go here
}
Custom deserializer
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.StdNodeBasedDeserializer;
import java.io.IOException;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.StreamSupport;
public class ThirdPartyTypeDeserializer
extends StdNodeBasedDeserializer<ThirdPartyType> {
protected ThirdPartyTypeDeserializer() {
super(ThirdPartyType.class);
}
#Override
public ThirdPartyType convert(JsonNode root, DeserializationContext ctxt)
throws IOException {
return new ThirdPartyType(
root.get("stringProperty").asText(null),
root.get("intProperty").asInt(),
StreamSupport.stream(
Spliterators.spliteratorUnknownSize(
root.get("arrayProperty").elements(),
Spliterator.ORDERED),
false).toArray());
}
}
Type containing the third party type
public class EnclosingClass {
#JsonDeserialize(using = ThirdPartyTypeDeserializer.class)
private ThirdPartyType thirdPartyProperty;
// Getters and setters go here
}
Retrieving the value
String json = "{\"thirdPartyProperty\": {"
+ "\"stringProperty\": \"A\", "
+ "\"intProperty\": 5, "
+ "\"arrayProperty\": [1, \"B\", false]"
+ "}}";
ObjectMapper objectMapper = new ObjectMapper();
EnclosingClass enclosingClass =
objectMapper.readValue(json, EnclosingClass.class);

Categories

Resources