Custom JSON Deserialization with Jackson

Custom JSON Deserialization with Jackson

You can write custom deserializer for this class. It could look like this:

class FlickrAccountJsonDeserializer extends JsonDeserializer<FlickrAccount> {

@Override
public FlickrAccount deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Root root = jp.readValueAs(Root.class);

FlickrAccount account = new FlickrAccount();
if (root != null && root.user != null) {
account.setId(root.user.id);
if (root.user.username != null) {
account.setUsername(root.user.username.content);
}
}

return account;
}

private static class Root {

public User user;
public String stat;
}

private static class User {

public String id;
public UserName username;
}

private static class UserName {

@JsonProperty("_content")
public String content;
}
}

After that, you have to define a deserializer for your class. You can do this as follows:

@JsonDeserialize(using = FlickrAccountJsonDeserializer.class)
class FlickrAccount {
...
}

How to perform a custom JSON deserialization with Jackson that respects a custom annotation?

Method hasIgnoreMarker is called not only for fields, but also for the constructor, including the virtual one:

Method called to check whether given property is marked to be ignored. This is used to determine whether to ignore properties, on per-property basis, usually combining annotations from multiple accessors (getters, setters, fields, constructor parameters).

In this case you should ignore only fields, that are not marked properly:

static class CustomAnnotationIntrospector extends JacksonAnnotationIntrospector {
@Override
public PropertyName findNameForDeserialization(Annotated a) {
Property property = a.getAnnotation(Property.class);
if (property == null) {
return PropertyName.USE_DEFAULT;
} else {
return PropertyName.construct(property.name());
}
}

@Override
public boolean hasIgnoreMarker(AnnotatedMember m) {
return m instanceof AnnotatedField
&& m.getAnnotation(Property.class) == null;
}
}

Example:

class Pojo {
// @Property(name = "id")
Integer id;
// @Property(name = "number")
Integer number;
@Property(name = "assure")
Boolean assure;
@Property(name = "person")
Map<String, String> person;
}
String json =
"{\"id\" : 1, \"number\" : 12345, \"assure\" : true," +
" \"person\" : {\"name\" : \"John\", \"age\" : 23}}";

ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new CustomAnnotationIntrospector());
Pojo pojo = mapper.readValue(json, Pojo.class);

System.out.println(pojo);
Pojo{id=null, number=null, assure=true, person={name=John, age=23}}

Note: Custom Property annotation should have RetentionPolicy.RUNTIME (same as JsonProperty annotation):

@Retention(RetentionPolicy.RUNTIME)
public @interface Property {
String name();
}

Custom jackson deserializer only for nested class

You can add a constructor to Vehicle class using @JsonCreator tag and a Double[] for parameters start and end. You also need to add @JsonProperty tag to each parameter.

Here's an example, for simplicity I didn't include parameters time_window and breaks.

@JsonCreator
public Vehicle(@JsonProperty("id") Integer id,
@JsonProperty("description") String description,
@JsonProperty("start") Double[] start,
@JsonProperty("end") Double[] end,
@JsonProperty("capacity") List<Integer> capacity,
@JsonProperty("skills") List<Integer> skills) {
this(id, description, new Location(start[0], start[1]),
new Location(end[0], end[1]), capacity, skills);
}

Test using the json in the question without parameters time_window and breaks:

String result = "{\"id\":0,\"description\":\"vehicle 0\",\"start\":[12.304373066846503,51.62270653765847],\"end\":[12.304373066846503,51.62270653765847],\"capacity\":[9],\"skills\":[]}";

ObjectMapper mapper = new ObjectMapper();

Vehicle vehicle = mapper.readValue(result, Vehicle.class);

String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vehicle);

System.out.println(json);

Output:

{
"id" : 0,
"description" : "vehicle 0",
"start" : {
"latitude" : 12.304373066846503,
"longitude" : 51.62270653765847
},
"end" : {
"latitude" : 12.304373066846503,
"longitude" : 51.62270653765847
},
"capacity" : [ 9 ],
"skills" : [ ]
}

How to override custom Jackson deserializer in child project?

I had to implement BeanPostProcessor and override it there:

@Configuration
public class FinalJacksonConfiguration implements BeanPostProcessor {

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof Jackson2ObjectMapperBuilder) {
final Jackson2ObjectMapperBuilder builder = (Jackson2ObjectMapperBuilder) bean;
builder.serializerByType(MyInterface.class, new ChildMyInterfaceDeserializer());
}
return bean;
}
}

Custom Deserializer in Json

There are two methods to solve your issue:

Method 1: use a middle object to mapper: I have created a demo for your issue see this.

Create a middle object to mapper the original JSON

public class SfPojoDto {
private String device;
private List<Ifce> ifces;
}

public class Ifce {
private String port;
private String reservableBW;
private String[] capabilites;
}

Then use custom deserializer to mapper it firstly and convert to your target object.

public class DeviceDeserializer extends JsonDeserializer<SfPojo> {
@Override
public SfPojo deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// while (p.nextToken()!=null) {
// if (p.getCurrentToken()==)
// }
String temp = p.readValueAsTree().toString();
ObjectMapper mapper = new ObjectMapper();
SfPojoDto sfPojoDto = mapper.readValue(temp, SfPojoDto.class);
SfPojo sfPojo = new SfPojo();
sfPojo.setDevice(sfPojoDto.getDevice());
List<Ifce> ifceList = sfPojoDto.getIfces();
for (Ifce ifce : ifceList) {
List<String> capabilities = Arrays.asList(ifce.getCapabilites());
if (capabilities.contains("ETHERNET")) {
sfPojo.setPort(ifce.getPort());
sfPojo.setReservbleBw(ifce.getReservableBW());
return sfPojo;
}
}
return sfPojo;
}
}

Method 2: you can use the JsonParser to operate the JSON string, but this method is more complicated, you can ref this article:

Jackson Custom Deserializer for polymorphic objects and String literals as defaults

This was actually easier than I thought to solve it. I got it working using the following:

  1. Custom deserializer implementation:
public class VehicleDeserializer extends StdDeserializer<Vehicle> {

public VehicleDeserializer() {
super(Vehicle.class);
}

@Override
public Vehicle deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
if (jp.currentToken() == JsonToken.VALUE_STRING) {
Car car = new Car();
car.setName(jp.readValueAs(String.class));
return car;
}
return jp.readValueAs(Vehicle.class);
}
}

  1. To avoid circular dependencies and to make the custom deserializer work with the polymorphic @JsonTypeInfo and @JsonSubTypes annotations I kept those annotations on the class level of Vehicle, but put the following annotations on the container object I am deserializing:
public class Transport {

@JsonDeserialize(using = VehicleDeserializer.class)
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
private Vehicle modeOfTransport;

// Getter, setters
}

This means that by default a Vehicle is deserialized as a polymorphic object, unless explicitly specified to deserialize it using my custom deserializer. This deserializer will then in turn defer to the polymorphism if the input is not a String.

Hopefully this will help someone running into this issue :)

Custom deserialisation of JSON field using Jackson

@JsonDeserialize annotation can be placed on a field, a setter or a class. Jackson will take it into account if what is annotated is what it uses to set the value.

E.g.1 It will notice @JsonDeserialize over a setter if it uses the setter to set the value of a field.

E.g.2 It will notice @JsonDeserialize over a field if it directly sets this field without using a setter or a constructor.

It will tend to take it into account if it's on a class unless it's overridden by a more specific annotation on a field or setter docs. I reckon the docs could be clearer on the above details.

In your case you have the annotation over the SpecialProperty field but you are setting this field in the MyClass constructor so it's ignored.

In this case you can move @JsonDeserialize over the class instead of over the field. That's probably the simplest solution in your case. E.g.

@JsonDeserialize(using = MyClass.SpecialPropertyDeserializer.class)
private static class SpecialProperty {

Or you can skip the annotation altogether and register the deserializer on the mapper. First make SpecialProperty and SpecialPropertyDeserializer non private in MyClass and then:

ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(MyClass.SpecialProperty.class, new MyClass.SpecialPropertyDeserializer());
objectMapper.registerModule(module);

You can also get rid of constructor of MyClass and the current annotation over the SpecialProperty field will be taken into account.

Swap Jackson custom serializer / deserializer during runtime

This is my solution

It's not pretty but does its job.

I left my old jackson config untouched, so the client<->server serialization stays the same.
I then added this custom ObjectMapper to take care of my server<->file.

My custom ObjectMapper does the following things:

  1. It registers a new custom JacksonAnnotationIntrospector, which I configured to ignore certain annotations. I also configured it to use my selfmade annotation @TransferJsonTypeInfo whenever a property has both the @TransferJsonTypeInfo as well as the @JsonTypeInfo annotation.
  2. I registered my CustomerFileSerializer and CustomerFileDeserializer for this ObjectMapper.
@Service
public class ImportExportMapper {

protected final ObjectMapper customObjectMapper;

private static final JacksonAnnotationIntrospector IGNORE_JSON_ANNOTATIONS_AND_USE_TRANSFERJSONTYPEINFO = BuildImportExportJacksonAnnotationIntrospector();

public ImportExportMapper(){
customObjectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);

// emulate the default settings as described here: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-customize-the-jackson-objectmapper
customObjectMapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
customObjectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

SimpleModule module = new SimpleModule();
module.addSerializer(Customer.class, new CustomerFileSerializer());
module.addDeserializer(Customer.class, new CustomerFileDeserializer());

customObjectMapper.setAnnotationIntrospector(IGNORE_JSON_ANNOTATIONS_AND_USE_TRANSFERJSONTYPEINFO);

customObjectMapper.registerModule(module);
}

public String writeValueAsString(Object data) {
try {
return customObjectMapper.writeValueAsString(data);
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new IllegalArgumentException();
}
}

public ObjectTransferData readValue(String fileContent, Class clazz) throws JsonProcessingException {
return customObjectMapper.readValue(fileContent, clazz);
}

private static JacksonAnnotationIntrospector BuildImportExportJacksonAnnotationIntrospector() {
return new JacksonAnnotationIntrospector() {

@Override
protected <A extends Annotation> A _findAnnotation(final Annotated annotated, final Class<A> annoClass) {
if (annoClass == JsonTypeInfo.class && _hasAnnotation(annotated, FileJsonTypeInfo.class)) {
FileJsonTypeInfo fileJsonTypeInfo = _findAnnotation(annotated, TransferJsonTypeInfo.class);
if(fileJsonTypeInfo != null && fileJsonTypeInfo.jsonTypeInfo() != null) {
return (A) fileJsonTypeInfo.jsonTypeInfo(); // this cast should be safe because we have checked the annotation class
}
}
if (ignoreJsonAnnotations(annoClass)) return null;
return super._findAnnotation(annotated, annoClass);
}
};
}

private static <A extends Annotation> boolean ignoreJsonAnnotations(Class<A> annoClass) {
if (annoClass == JsonSerialize.class) {
return true;
}
if(annoClass == JsonDeserialize.class){
return true;
}
if(annoClass == JsonIdentityReference.class){
return true;
}
return annoClass == JsonIdentityInfo.class;
}
}

My custom annotation is defined and described like this:

/**
* This annotation inside of a annotation solution is a way to tell the importExportMapper how to serialize/deserialize
* objects that already have a wrongly defined @JsonTypeInfo annotation (wrongly defined for the importExportMapper).
*
* Idea is taken from here: https://stackoverflow.com/questions/58495480/how-to-properly-override-jacksonannotationintrospector-findannotation-to-replac
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FileJsonTypeInfo {
JsonTypeInfo jsonTypeInfo();
}

And it is used like this:

    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
@JsonTypeInfo(defaultImpl = Customer.class, property = "", use = JsonTypeInfo.Id.NONE)
@TransferJsonTypeInfo(jsonTypeInfo = @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "customeridentifier"))
@JsonIdentityReference(alwaysAsId = true)
@JsonDeserialize(using = CustomerClientDeserializer.class)
@JsonSerialize(using = CustomerClientSerializer.class)
private Customer customer;


Related Topics



Leave a reply



Submit