Ags
Ags

Reputation: 95

How to differentiate null value fields from absent fields in jackson library

We are consuming an API and the api is providing xml fields. We have to convert xml to json for our consumers. We have a requirement of showing just what we have got as XML and to display only those fields .

  1. If fields exists with value present it
  2. If fields does not exist don't present it
  3. if field exists with null/no value display the field as it is .

What I have seen is general annotations

@JsonInclude(NON_EMPTY) can be used to exclude values that are empty.I cannot use this because I still want to see the empty fields with null value in json

@JsonInclude(NON_ABSENT) can be used to exclude null values and values that are "absent".I cannot use this because I still want to see the empty fields and null fields in json. Same with the JsonInclude (NON_NULL)

So my question is if I don't specify any of these properties can I achieve what I want ? In other words if I don't specify any of these is jackson's behavior is to show all the fields that are have the null value on dynamic sense ? My major concern is the dynamic response here . For each requests the fields may be present or not be present . We have to show in the json what exactly we receive in XML

Upvotes: 6

Views: 4111

Answers (1)

Michał Ziober
Michał Ziober

Reputation: 38700

If you want to differentiate null value fields from absent fields the most generic method will be using Map or JsonNode instead of POJO. POJO class has constant structure, Map or JsonNode have dynamic - contains only what you actually put there. Let's create a simple app which reads XML payload from file and creates JSON response:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;

import java.io.File;
import java.util.Map;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        File xmlFile = new File("./resource/test.xml").getAbsoluteFile();

        XmlMapper xmlMapper = new XmlMapper();
        Map map = xmlMapper.readValue(xmlFile, Map.class);

        ObjectMapper jsonMapper = new ObjectMapper();
        String json = jsonMapper.writeValueAsString(map);

        System.out.println(json);
    }
}

Now take a look on some examples where we test what JSON will be generated for empty, null and absent nodes.

Test 0-0

Input XML:

<Root>
    <a>A</a>
    <b>1</b>
    <c>
        <c1>Rick</c1>
        <c2>58</c2>
    </c>
</Root>

Result JSON is:

{"a":"A","b":"1","c":{"c1":"Rick","c2":"58"}}

Test 0-1

Input XML:

<Root>
    <a>A</a>
    <c>
        <c1>Rick</c1>
        <c2/>
    </c>
</Root>

Output JSON:

{"a":"A","c":{"c1":"Rick","c2":null}}

Test 0-2

Input XML:

<Root>
    <c/>
</Root>

Output JSON:

{"c":null}

The biggest problem with this simple and fast solution is we lost type information for primitives. For example, if b is Integer we should return it in JSON as number primitive which does not have quotes: " chars around. To solve this problem we should use POJO model which allows us to find all required types. Let's create POJO model for our example:

@JsonFilter("allowedFields")
class Root {
    private String a;
    private Integer b;
    private C c;

    // getters, setters
}

@JsonFilter("allowedFields")
class C {
    private String c1;
    private Integer c2;

    // getters, setters
}

We need to change our simple XML -> Map -> JSON algorithm to below one:

  1. Read JSON as Map or JsonNode
  2. Find all field names
  3. Create FilterProvider with found names - notice that filter is registered with allowedFields name, the same which is used in @JsonFilter annotation.
  4. Convert Map to POJO for type coercion.
  5. Write POJO with filter

Simple app could look like this:

import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;

import java.io.File;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        File xmlFile = new File("./resource/test.xml").getAbsoluteFile();

        NodesWalker walker = new NodesWalker();

        XmlMapper xmlMapper = new XmlMapper();
        JsonNode root = xmlMapper.readValue(xmlFile, JsonNode.class);
        Set<String> names = walker.findAllNames(root);

        SimpleFilterProvider filterProvider = new SimpleFilterProvider();
        filterProvider.addFilter("allowedFields", SimpleBeanPropertyFilter.filterOutAllExcept(names));

        ObjectMapper jsonMapper = new ObjectMapper();
        jsonMapper.setFilterProvider(filterProvider);

        Root rootConverted = jsonMapper.convertValue(root, Root.class);
        String json = jsonMapper.writeValueAsString(rootConverted);

        System.out.println(json);
    }
}

class NodesWalker {

    public Set<String> findAllNames(JsonNode node) {
        Set<String> names = new HashSet<>();

        LinkedList<JsonNode> nodes = new LinkedList<>();
        nodes.add(node);
        while (nodes.size() > 0) {
            JsonNode first = nodes.removeFirst();
            if (first.isObject()) {
                ObjectNode objectNode = (ObjectNode) first;
                objectNode.fields().forEachRemaining(e -> {
                    names.add(e.getKey());
                    JsonNode value = e.getValue();
                    if (value.isObject() || value.isArray()) {
                        nodes.add(value);
                    }
                });
            } else if (first.isArray()) {
                ArrayNode arrayNode = (ArrayNode) first;
                arrayNode.elements().forEachRemaining(e -> {
                    if (e.isObject() || e.isArray()) {
                        nodes.add(e);
                    }
                });
            }
        }

        return names;
    }
}

Test 1-0

Input XML:

<Root>
    <a>A</a>
    <b>1</b>
    <c>
        <c1>Rick</c1>
        <c2>58</c2>
    </c>
</Root>

Output JSON:

{"a":"A","b":1,"c":{"c1":"Rick","c2":58}}

Test 1-1

Input XML:

<Root>
    <b>1</b>
    <c>
        <c2/>
    </c>
</Root>

Output JSON:

{"b":1,"c":{"c2":null}}

Test 1-2

Input XML:

<Root>
    <c/>
</Root>

Output JSON:

{"c":null}

After all these tests we see that dynamic checking whether field is null, empty or absent is not an easy task. Even so, above 2 solutions work for simple models you should test them for all responses you want to generate. When model is complex and contains many complex annotations such as: @JsonTypeInfo, @JsonSubTypes on Jackson side or @XmlElementWrapper, @XmlAnyElement on JAXB side it make this task very hard to implement.

I think the best solution in your example is to use @JsonInclude(NON_NULL) which send to client all set fields on XML side. null and absent should be treated on client side identically. Business logic should not rely on the fact field is set to null or absent in JSON payload.

See also:

Upvotes: 2

Related Questions