Home > Blockchain >  Jackson use the first non-null alias
Jackson use the first non-null alias

Time:12-14

I'm using jackson to parse json data on a springboot application, but jackson is unable to select properly the json field to map to java field...

First of all, i'm reading data from a thirdy party library which sucks, their data model is a piece of .... and i can't ask them to fix.

their json is something like:

{ "name" : "name", "full_name" : "name", "fullName" : "name"}

they have 3 properties that map to same information in the json, but sometimes only one of these properties is non null, like

{ "name" : null, "full_name" : "", "fullName" : "name"}

Let be clear that this happens to many other properties

I'm trying to use @JsonAlias to map this weird behavior, but it doesn't work well....

@JsonProperty("name")
@JsonAlias({"full_name","fullName"})
private String name;

First that JsonAlias is taking precendence from the JsonProperty value...
second that is not ignoring null values

how can i make jackson ignore null values in a situation like this

CodePudding user response:

Multiple Setters

One of the possible options is to define a bunch of additional setters for each duplicated property, annotated with @JsonSetter to map each setter to a corresponding property flavor.

To avoid logic duplication, every additional setter should delegate to a regular setter, which should determine if the existing value needs to be updated (if it's empty, null, etc.).

public class MyPojo {
    public static final Predicate<String> NULL_OR_EMPTY =
        s -> s == null || s.isEmpty(); // predicate can be reused to check multiple properties

    private String name;
    
    @JsonSetter("name")
    public void setName(String name) {
        if (NULL_OR_EMPTY.test(this.name)) this.name = name;
    }

    @JsonSetter("full_name")
    public void setName1(String name) {
        setName(name);
    }

    @JsonSetter("fullName")
    public void setName2(String name) {
        setName(name);
    }
}

Usage example:

String json = "{ "name" : null, "full_name" : "", "fullName" : "name"}";
    
ObjectMapper mapper = new ObjectMapper();
        
MyPojo myPojo = mapper.readValue(json, MyPojo.class);
System.out.println(myPojo);

Output:

MyPojo{name='name'}

The drawbacks of this approach are the usage of "smart-setters", which might not considered to be clean (because it violates the Single responsibility principle, setters aren't meant to perform validation) and domain class gets polluted with additional setter methods. Both issues can be solved by externalizing this functionality.

Custom Converter

Another possible solution is to customize deserialization by defining a Converter for the target type.

Note: don't confuse Converter and Deserializer. Deserializer is meant to provide logic how to construct a POJO based on the information contained in the JsonParser, whilst Converter is used to transform one POJO (which is easier to deserialize) into another POJO.

So we need to create two classes: Converter and auxiliary type reflecting the data model of the incoming JSON.

Consider the following auxiliary POJO:

public record AuxiliaryPojo(
    @JsonProperty("name") String name,
    @JsonProperty("full_name") String name1,
    @JsonProperty("fullName") String name2
) {}

And that's the Converter extending StdConverter that bridges AuxiliaryPojo and MyPojo:

public class AuxiliaryPojoToMyPojo extends StdConverter<AuxiliaryPojo, MyPojo> {
    public static final Predicate<String> NOT_NULL_OR_EMPTY =
        s -> s != null && !s.isEmpty();

    @Override
    public MyPojo convert(AuxiliaryPojo v) {
        return MyPojo.builder()
            .name(findMatching(v.name(), v.name1(), v.name2()))
            .build();
    }
    
    private String findMatching(String... args) {
        return Arrays.stream(args)
            .filter(NOT_NULL_OR_EMPTY)
            .findFirst().orElse(null);
    }
}

And here's the domain class (free from any redundant code). Note that Converter has been specified via converter property of the @JsonDeserialize annotation.

@Getter
@Builder
@JsonDeserialize(converter = AuxiliaryPojoToMyPojo.class)
public static class MyPojo {
    private String name;
}

That would be enough to parse the sample JSON into an instance of MyPojo:

String json = "{ "name" : null, "full_name" : "", "fullName" : "name"}";
    
ObjectMapper mapper = new ObjectMapper();
        
MyPojo myPojo = mapper.readValue(json,bMyPojo.class);
System.out.println(myPojo);

Output:

MyPojo{name='name'}

CodePudding user response:

An other solution is to ignore additionnal fields 'fullName' and 'full_name' using annotation @JsonIgnoreProperties and a custom deserializer that throws an exception when name property is null (or equal to string "null") so that you can catch that exception in order not to create a person when name is null.

See code below:

Class Person

import java.util.ArrayList;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

@JsonIgnoreProperties({ "fullName", "full_name" })
public class Person {
    @JsonProperty("name")
    @JsonDeserialize(using = CustomNameDeserializer.class)
    private String name;

    public Person() {
        super();
    }

    public String toString() {
        return "name: "   name;
    }

    public static void main(String[] args) {
        String[] personArrayJson = new String[3];
        personArrayJson[0]="{ \"name\" : \"nameNotNull1\", \"full_name\" : \"name\", \"fullName\" : \"name\"}";
        personArrayJson[1]="{ \"name\" : \"null\", \"full_name\" : \"\", \"fullName\" : \"name\"}";
        personArrayJson[2]="{ \"name\" : \"nameNotNull2\", \"full_name\" : \"name\", \"fullName\" : \"name\"}";

        List<Person> persons = new ArrayList<Person>();
        ObjectMapper mapper = new ObjectMapper();
        Person p;
        for (String personJson : personArrayJson) {
            try {
                p = mapper.readValue(personJson, Person.class);
                persons.add(p);
            } catch (JsonProcessingException e) {
                //e.printStackTrace();
                System.out.println("NAme is null: not deserialized");
            }
        }
        
        System.out.println("Persons list contains " persons.size() " persons => " persons);
    }
}

Class CustomNameDeserializer

import java.io.IOException;

import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;

public class CustomNameDeserializer extends StdDeserializer<String> {

    public CustomNameDeserializer(Class<String> s) {
        super(s);
    }

    public CustomNameDeserializer() {
        this(null);
    }

    @Override
    public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException, NullPointerException{
        String n = p.getValueAsString();
        if(n==null || n.equals("null") || n.trim().equals(""))
            throw new NullPointerException("Name is null");
        return n;
    }
}

Hope it helps

  • Related