Jackson deserialize JSON object field into single list property - java

I was just wondering, given a pojo:
public class MyProfileDto {
private List<String> skills;
//mutators; getSkills; setSkills + bunch of other fields
}
and JSON for the skills field:
"skills":{
"values":[
{
"id":14,
"skill":{
"name":"C++"
}
},
{
"id":15,
"skill":{
"name":"Java"
}
}
],
"_total":2
}
Is there any way using Jackson to get the skills/values/skill/name field (i.e. "Java", "C++") into the target Dto's String List without creating a custom deserializer for the entire Dto? It has many fields so an ideal solution would involve some custom annotation or deserializer for the one field if possible??

Jackson does not contains any XPath feature but you can define converter for each property. This converter will be used by Jackson to convert input type to output type which you need. In your example input type is Map<String, Object> and output type is List<String>. Probably, this is not the simplest and the best solution which we can use but it allows us to define converter for only one property without defining deserializer for entire entity.
Your POJO class:
class MyProfileDto {
#JsonDeserialize(converter = SkillConverter.class)
private List<String> skills;
public List<String> getSkills() {
return skills;
}
public void setSkills(List<String> skills) {
this.skills = skills;
}
}
Converter for List<String> skills; property:
class SkillConverter implements Converter<Map<String, Object>, List<String>> {
#SuppressWarnings("unchecked")
public List<String> convert(Map<String, Object> value) {
Object values = value.get("values");
if (values == null || !(values instanceof List)) {
return Collections.emptyList();
}
List<String> result = new ArrayList<String>();
for (Object item : (List<Object>) values) {
Map<String, Object> mapItem = (Map<String, Object>) item;
Map<String, Object> skillMap = (Map<String, Object>) mapItem.get("skill");
if (skillMap == null) {
continue;
}
result.add(skillMap.get("name").toString());
}
return result;
}
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructMapLikeType(Map.class, String.class, Object.class);
}
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructCollectionLikeType(List.class, String.class);
}
}
And example usage:
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.Converter;
public class JacksonProgram {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
MyProfileDto dto = mapper.readValue(new File("/x/json"), MyProfileDto.class);
System.out.println(dto.getSkills());
}
}
Above program prints:
[C++, Java]

Related

Dynamic Json To Java Pojo

I want to map the following json to a pojo in Java. In the snippet shown below, result is a Json object, whose value is another json object which is a map. I tried converting this to a Pojo, but it failed. The keys in the result map are dynamic, and I cannot guess them prior.
final_result :
{
"result":
{
"1597696140": 70.32,
"1597696141": 89.12,
"1597696150": 95.32,
}
}
The pojo that I created is :
#JsonIgnoreProperties(ignoreUnknown = true)
public class ResultData {
Map<Long, Double> resultMap;
public ResultData(Map<Long, Double> resultMap) {
this.resultMap = resultMap;
}
public ResultData() {
}
#Override
public String toString() {
return super.toString();
}
}
Upon trying to create the pojo using ObjectMapper :
ObjectMapper objectMapper = new ObjectMapper();
ResultData resultData = objectMapper.readValue(resultData.getJSONObject("result").toString(), ResultData.class);
What am I possible doing wrong here ?
Assume, your JSON payload looks like below:
{
"final_result": {
"result": {
"1597696140": 70.32,
"1597696141": 89.12,
"1597696150": 95.32
}
}
}
You can deserialise it to class:
#JsonRootName("final_result")
class ResultData {
private Map<Long, Double> result;
public Map<Long, Double> getResult() {
return result;
}
#Override
public String toString() {
return result.toString();
}
}
Like below:
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.util.Map;
public class Main {
public static void main(String[] args) throws IOException {
File jsonFile = new File("./src/main/resources/test.json");
ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.UNWRAP_ROOT_VALUE);
ResultData resultData = mapper.readValue(jsonFile, ResultData.class);
System.out.println(resultData);
}
}
Above code prints:
{1597696140=70.32, 1597696141=89.12, 1597696150=95.32}
Converting the JSONObject to Map and setting the map to the pojo field, solved the issue and didn't lead me to writing a custom deserializer.
Map<Long, Double> resultData = objectMapper.readValue(resultData.getJSONObject("result").toString(), Map.class);
FinalResultData finaResultData = new FinalResultData(resultData);

Different fields with same JsonProperty attribute

Is it possible to have something like below while serializing a JSON in the same class
#JsonProperty("stats")
private StatsDetails statsDetails
#JsonProperty("stats")
private List<StatsDetails> statsDetailsList
so i can have either statsDetails or statsDetailsList only one of these being included while forming a json.
I also have a separate JsonMapper code that transforms this pojo data into a json which i haven't included here.
You cannot do that. It will throw JsonMappingException jackson cannot know which of the fields are you referring to. You can try it by yourself with the following code:
POJOClass:
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import java.util.List;
public class POJOClass {
public POJOClass(String object) {
this.object = object;
}
public POJOClass(List<String> objectList) {
this.objectList = objectList;
}
#JsonProperty("object")
public String object;
#JsonProperty("object")
public List<String> objectList;
#JsonGetter("object")
public String getObject() {
return object;
}
#JsonGetter("object")
public List<String> getObjectList() {
return objectList;
}
#JsonSetter("object")
public void setObject(String object) {
this.object = object;
}
#JsonSetter("object")
public void setObjectList(List<String> objectList) {
this.objectList = objectList;
}
}
Main class:
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
public class MainClass {
public static void main(String[] args) {
String text = "f";
List<String> list = Arrays.asList("a", "b", "c");
ObjectMapper mapper = new ObjectMapper();
try {
String json = mapper.writeValueAsString(new POJOClass(text));
String listJson = mapper.writeValueAsString(new POJOClass(list));
System.out.println("json=" + json);
System.out.println("listJson=" + listJson);
} catch (IOException e) {
e.printStackTrace();
}
}
}
The output:
com.fasterxml.jackson.databind.JsonMappingException: Multiple fields representing property "object": POJOClass#object vs POJOClass#objectList

With default typing enabled, is there a way to override the type Jackson outputs?

I am serializing a class that includes an unmodifiable list with default typing enabled. The problem is that the type that Jackson uses is
java.util.Collections$UnmodifiableRandomAccessList
which, for some reason, the deserializer does not know how to handle.
Is there a way to tell Jackson to set the type as
java.util.ArrayList
which the deserializer does know how to handle, instead? If possible, I'd like to do it using mixins.
Something like
public abstract class ObjectMixin {
#JsonCreator
public ObjectMixin(
#JsonProperty("id") String id,
#JsonProperty("list") #JsonSerialize(as = ArrayList.class) List<String> list;
) {}
}
which, unfortunately, does not work.
I would like to start from security risk warning which comes from ObjectMapper documentation:
Notes on security: use "default typing" feature (see
enableDefaultTyping()) is a potential security risk, if used with
untrusted content (content generated by untrusted external parties).
If so, you may want to construct a custom TypeResolverBuilder
implementation to limit possible types to instantiate, (using
setDefaultTyping(com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder<?)).
Lets implement custom resolver:
class CollectionsDefaultTypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder {
private final Map<String, String> notValid2ValidIds = new HashMap<>();
public CollectionsDefaultTypeResolverBuilder() {
super(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
this._idType = JsonTypeInfo.Id.CLASS;
this._includeAs = JsonTypeInfo.As.PROPERTY;
notValid2ValidIds.put("java.util.Collections$UnmodifiableRandomAccessList", ArrayList.class.getName());
// add more here...
}
#Override
protected TypeIdResolver idResolver(MapperConfig<?> config, JavaType baseType, Collection<NamedType> subtypes,
boolean forSer, boolean forDeser) {
return new ClassNameIdResolver(baseType, config.getTypeFactory()) {
#Override
protected String _idFrom(Object value, Class<?> cls, TypeFactory typeFactory) {
String id = notValid2ValidIds.get(cls.getName());
if (id != null) {
return id;
}
return super._idFrom(value, cls, typeFactory);
}
};
}
}
Now, we can use it as below:
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.impl.ClassNameIdResolver;
import com.fasterxml.jackson.databind.type.TypeFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JsonApp {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.setDefaultTyping(new CollectionsDefaultTypeResolverBuilder());
Root root = new Root();
root.setData(Collections.unmodifiableList(Arrays.asList("1", "b")));
String json = mapper.writeValueAsString(root);
System.out.println(json);
System.out.println(mapper.readValue(json, Root.class));
}
}
class Root {
private List<String> data;
public List<String> getData() {
return data;
}
public void setData(List<String> data) {
this.data = data;
}
#Override
public String toString() {
return "Root{" +
"data=" + data +
'}';
}
}
Above code prints:
{
"data" : [ "java.util.ArrayList", [ "1", "b" ] ]
}
Root{data=[1, b]}
You can even map it to List interface:
notValid2ValidIds.put("java.util.Collections$UnmodifiableRandomAccessList", List.class.getName());
And output would be:
{
"data" : [ "java.util.List", [ "1", "b" ] ]
}

Jackson XML Bind Element based on value of attributes

I have the following XML structure:
<participants>
<participant side="AWAY">
<team id="18591" name="Orlando Apollos" />
</participant>
<participant side="HOME">
<team id="18594" name="Memphis Express" />
</participant>
</participants>
If I am using the FasterXML Jackson library with JAXB annotations how can I bind the participants fields to two different Participant objects participantHome and participantAway using the side property of AWAY and HOME to bind the fields.
Using the following object won't work obviously because there are duplicate fields:
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
#XmlRootElement(name = "participants")
public class Participants {
#XmlElement(name = "participant")
Participant participantHome;
#XmlElement(name = "participant")
Participant participantAway;
}
How can I dynamically bind those elements using JAXB annotations or a custom JAXB implementation?
You need to write custom deserialiser because there is no annotation which allow to bind list item to given property in object. If you already use Jackson try to implement custom JsonDeserializer instead of custom XmlAdapter. We can simplify our custom deserialiser by deserialising internal Participant objects to Map. Simple example:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.type.MapType;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class XmlMapperApp {
public static void main(String[] args) throws Exception {
File xmlFile = new File("./resource/test.xml").getAbsoluteFile();
XmlMapper xmlMapper = new XmlMapper();
Participants result = xmlMapper.readValue(xmlFile, Participants.class);
System.out.println(result);
}
}
class ParticipantsXmlAdapter extends JsonDeserializer<Participants> {
#Override
public Participants deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
List<Map<String, Object>> participants = readParticipantsMap(p, ctxt);
Participants result = new Participants();
for (Map<String, Object> participantMap : participants) {
Object side = participantMap.get("side").toString();
if ("AWAY".equals(side)) {
result.setParticipantAway(convert((Map<String, Object>) participantMap.get("team")));
} else if ("HOME".equals(side)) {
result.setParticipantHome(convert((Map<String, Object>) participantMap.get("team")));
}
}
return result;
}
private List<Map<String, Object>> readParticipantsMap(JsonParser p, DeserializationContext ctxt) throws IOException {
MapType mapType = ctxt.getTypeFactory().constructMapType(Map.class, String.class, Object.class);
JsonDeserializer<Object> mapDeserializer = ctxt.findRootValueDeserializer(mapType);
List<Map<String, Object>> participants = new ArrayList<>();
p.nextToken(); // skip Start of Participants object
while (p.currentToken() == JsonToken.FIELD_NAME) {
p.nextToken(); // skip start of Participant
Object participant = mapDeserializer.deserialize(p, ctxt);
participants.add((Map<String, Object>) participant);
p.nextToken(); // skip end of Participant
}
return participants;
}
private Participant convert(Map<String, Object> map) {
Participant participant = new Participant();
participant.setId(Integer.parseInt(map.get("id").toString()));
participant.setName(map.get("name").toString());
return participant;
}
}
#JsonDeserialize(using = ParticipantsXmlAdapter.class)
class Participants {
private Participant participantHome;
private Participant participantAway;
// getters, setters, toString
}
class Participant {
private int id;
private String name;
// getters, setters, toString
}
prints:
Participants{participantHome=Participant{id=18594, name='Memphis Express'}, participantAway=Participant{id=18591, name='Orlando Apollos'}}
You can use List of Participant in place of two different participant.
Annotate side with #XmlAttribute(name = "side", required = true).
Then create two different Participant objects and add them to list.
Couple of great answers and alternatives here but I've decided to go with a hybrid of binding with a list and returning the correct home or away team by implementing getter methods that return the correct home or away team to essentially flatten the List. This will reduce the amount of computation when processing the lists throughout the application.
I added the following code to my parent class (to each home/away participant):
Participant getHome() {
return (Participant) participants.stream()
.filter(p -> p.getSide().equalsIgnoreCase("home"));
}
Participant getAway() {
return (Participant) participants.stream()
.filter(p -> p.getSide().equalsIgnoreCase("away"));
}
Thanks for the help!

How do I override a Java map when converting a JSON to Java Object using GSON?

I have a JSON string which looks like this:
{
"status": "status",
"date": "01/10/2019",
"alerts": {
"labels": {
"field1": "value1",
"field2": "value2",
"field3": "value3",
"field100": "value100"
},
"otherInfo" : "other stuff"
},
"description": "some description"
}
My corresponding Java classes look like the following:
public class Status {
private String status;
private String date;
private Alerts alerts;
private String description;
}
And
public class Alerts {
private Map<String, String> labels;
private String otherInfo;
public Map<String, String> getLabels() {
return labels();
}
}
I'm parsing the given JSON into Java object using this:
Status status = gson.fromJson(statusJSONString, Status.class);
This also gives me Alerts object from Status class:
Alerts alerts = status.getAlerts();
Here is my problem:
Let's consider the labels:
I want to make keys in the label map the case-insensitive. So for example, if the provided key/value pair is "field1" : "value1", or "Field1" : "value1" or "fIeLD1":"value1", I want to be able to retrieve them by simply calling alerts.getLabels.get("field1").
Ideally, I want to set the keys to be lowercase when the labels map is originally created. I looked into Gson deserialization examples, but I'm not clear exactly how to approach this.
There isnt really much you can do here. Even if you extended HashMap, the problem is that when the JSON is de-serialized, it doesn't call native methods. What you COULD do is the following, but it is rather cumbersome:
import java.util.HashMap;
public class HashMapCaseInsensitive extends HashMap<String, String> {
private boolean convertedToLower = false;
#Override
public String put(String key, String value) {
if(!convertedToLower){
convertToLower();
}
return super.put(key.toLowerCase(), value);
}
#Override
public String get(Object key) {
if(!convertedToLower){
convertToLower();
}
return super.get(key.toString().toLowerCase());
}
private void convertToLower(){
for(String key : this.keySet()){
String data = this.get(key);
this.remove(key);
this.put(key.toLowerCase(), data);
}
convertedToLower = true;
}
}
You can write your own MapTypeAdapterFactory which creates Map always with lowered keys. Our adapter will be based on com.google.gson.internal.bind.MapTypeAdapterFactory. We can not extend it because it is final but our Map is very simple so let's copy only important code:
class LowercaseMapTypeAdapterFactory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
TypeAdapter<String> stringAdapter = gson.getAdapter(TypeToken.get(String.class));
return new TypeAdapter<T>() {
#Override
public void write(JsonWriter out, T value) { }
#Override
public T read(JsonReader in) throws IOException {
JsonToken peek = in.peek();
if (peek == JsonToken.NULL) {
in.nextNull();
return null;
}
Map<String, String> map = new HashMap<>();
in.beginObject();
while (in.hasNext()) {
JsonReaderInternalAccess.INSTANCE.promoteNameToValue(in);
String key = stringAdapter.read(in).toLowerCase();
String value = stringAdapter.read(in);
String replaced = map.put(key, value);
if (replaced != null) {
throw new JsonSyntaxException("duplicate key: " + key);
}
}
in.endObject();
return (T) map;
}
};
}
}
Now, we need to inform that our Map should be deserialised with our adapter:
class Alerts {
#JsonAdapter(value = LowercaseMapTypeAdapterFactory.class)
private Map<String, String> labels;
private String otherInfo;
// getters, setters, toString
}
Assume that our JSON payload looks like below:
{
"status": "status",
"date": "01/10/2019",
"alerts": {
"labels": {
"Field1": "value1",
"fIEld2": "value2",
"fielD3": "value3",
"FIELD100": "value100"
},
"otherInfo": "other stuff"
},
"description": "some description"
}
Example usage:
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.internal.JsonReaderInternalAccess;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class GsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
Gson gson = new GsonBuilder().create();
Status status = gson.fromJson(new FileReader(jsonFile), Status.class);
System.out.println(status.getAlerts());
}
}
Above code prints:
Alerts{labels={field1=value1, field100=value100, field3=value3, field2=value2}, otherInfo='other stuff'}
This is really tricky solution and it should be used carefully. Do not use this adapter with much complex Map-es. From other side, OOP prefers much simple solutions. For example, create decorator for a Map like below:
class Labels {
private final Map<String, String> map;
public Labels(Map<String, String> map) {
Objects.requireNonNull(map);
this.map = new HashMap<>();
map.forEach((k, v) -> this.map.put(k.toLowerCase(), v));
}
public String getValue(String label) {
return this.map.get(label.toLowerCase());
}
// toString
}
Add new method to Alerts class:
public Map<String, String> toLabels() {
return new Labels(labels);
}
Example usage:
status.getAlerts().toLabels()
Which gives you a very flexible and secure behaviour.
Though this is not a very generic solution, however, I think this will serve your purpose.
I would like to suggest you create an adapter for Gson which can convert the map values for you. The adapter might look like the following.
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
final class GSONAdapter implements JsonDeserializer<String> {
private static final GSONAdapter instance = new GSONAdapter();
static GSONAdapter instance() {
return instance;
}
#Override
public String deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
// Here I am taking the elements which are starting with field
// and then returning the lowercase version
// so that the labels map is created this way
if (jsonElement.getAsString().toLowerCase().startsWith("field"))
return jsonElement.getAsString().toLowerCase();
else return jsonElement.getAsString();
}
}
Now just add the GsonBuilder to your Gson using the adapter and then try to parse the JSON. You should get all the values in the lower case as you wanted for the labels.
Please note that I am just taking the field variables in my concern and hence this is not a generic solution which will work for every key. However, if your keys have any specific format, this can be easily applied.
Gson gson = new GsonBuilder()
.registerTypeAdapter(String.class, GSONAdapter.instance())
.create();
Status status = gson.fromJson(statusJSONString, Status.class);
Alerts alerts = status.getAlerts();
Hope that solves your problem.

Categories