Reputation: 95
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 .
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
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.
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"}}
Input XML
:
<Root>
<a>A</a>
<c>
<c1>Rick</c1>
<c2/>
</c>
</Root>
Output JSON
:
{"a":"A","c":{"c1":"Rick","c2":null}}
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:
Map
or JsonNode
FilterProvider
with found names - notice that filter is registered with allowedFields
name, the same which is used in @JsonFilter
annotation.Map
to POJO
for type coercion.POJO
with filterSimple 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;
}
}
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}}
Input XML
:
<Root>
<b>1</b>
<c>
<c2/>
</c>
</Root>
Output JSON
:
{"b":1,"c":{"c2":null}}
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