Jackson Unmarshall custom object instead of LinkedHashMap - java

I have a Java object Results:
public class MetaData {
private List<AttributeValue<String,Object>> properties
private String name
...
... getters/setters ...
}
The AttributeValue class is a generic key-value class. It's possible different AttributeValue's are nested. The (value) Object will then be another AttributeValue and so forth.
Due to legacy reasons the structure of this object cannot be altered.
I have my JSON, which I try to map to this object.
All goes well for the regular properties. Also the first level of the list is filled with AttributeValues.
The problem is the Object. Jackson doesn't know how to interpret this nested behavior and just makes it a LinkedHashMap.
I'm looking for a way to implement custom behavior to tell Jackson this has to be a AttributeValue-object instead of the LinkedHashMap.
This is how I'm currently converting the JSON:
ObjectMapper om = new ObjectMapper();
MetaData metaData = om.readValue(jsonString, new TypeReference<MetaData>(){});
And this is example JSON. (this is obtained by serializing an existing MetaData object to JSON, I have complete control over this syntax).
{
"properties":[
{
"attribute":"creators",
"value":[
{
"attribute":"creator",
"value":"user1"
},{
"attribute":"creator",
"value":"user2"
}
]
},{
"attribute":"type",
"value": "question"
}
],
"name":"example"
}
(btw: I've tried the same using GSON, but then the object is a StringMap and the problem is the same. Solutions using GSON are also welcome).
edit In Using Jackson ObjectMapper with Generics to POJO instead of LinkedHashMap there is a comment from StaxMan:
"LinkedHashMap is only returned when type information is missing (or if Object.class is defined as type)."
The latter seems to be the issue here. Is there a way I can override this?

If you have control over the serialization, try calling enableDefaultTyping() on your mapper.
Consider this example:
Pair<Integer, Pair<Integer, Integer>> pair = new Pair<>(1, new Pair<>(1, 1));
ObjectMapper mapper = new ObjectMapper();
String str = mapper.writeValueAsString(pair);
Pair result = mapper.readValue(str, Pair.class);
Without enableDefaultTyping(), I would have str = {"k":1,"v":{"k":1,"v":1}} which would deserialize to a Pair with LinkedHashMap.
But if I enableDefaultTyping() on mapper, then str = {"k":1,"v":["Pair",{"k":1,"v":1}]} which then perfectly deserializes to Pair<Integer, Pair<...>>.

Related

Deserializing JSON using RestTemplate which can have changing key

I'm currently calling this API https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api#1/latest/currencies/gbp.json
This has the path param of gbp which then is included in the response
{
"date": "2021-09-14",
"gbp": {
// omitted for brevity
}
}
Im using the RestTemplate.getForObject method to make the GET HTTP request which is successful however i am not sure how i would go about typing the response.
I will be calling this url with multiple different path parameters. So for example https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api#1/latest/currencies/eur.json is valid which will result in a response of
{
"date": "2021-09-14",
"eur": {
// omitted for brevity
}
}
So its not as easy as typing a gbp property on the response as Map<String, Double> etc. And i dont want to create a different class for each possible response.
So my question basically is. How can i type this? I've tried to use a custom #JsonDeserialzer annotation on a class which represents the data however since it does not know the key that was a bit of a dead end.
Is the only way to achieve this by using a custom ObjectMapper where i can pass the key to a customer deserializer rather than using the annotation?
First, consider using WebClient instead of RestTemplate, if you're on spring framework 5 (or switch to the webflux stack).
You don't need a custom ObjectMapper, but can just loop the properties by using restTemplate.getForEntity, like:
ResponseEntity<String> response = restTemplate.getForEntity
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(response.getBody());
Iterator<Map.Entry<String, JsonNode>> it = root.fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> field = it.next();
System.out.println("key: " + field.getKey());
//Add some logic here ;)
//Probably you want the second property, if it exists ...
if (it.hasNext) {
field = it.next();
JsonNode currencyNode = field.getValue();
}
}
If you need or want an object, try the #JsonAnySetter - see fe. here Unstable behavior with Jackson Json and #JsonAnySetter

Serialize to JSON as name-value from String by Jackson ObjectMapper

I have some String, like:
String value = "123";
And when i serialize this string to json via ObjectMapper:
objectMapper.writeValueAsString(value);
Output is:
"123"
Is it possible to write String using either string name and string value? Desired output:
"value" : "123"
PS: i dont want to create DTO object with one field for serializing one String value.
you can also use the Jackson JsonGenerator
try (JsonGenerator generator = new JsonFactory().createGenerator(writer)) {
generator.writeStartObject();
generator.writeFieldName("value");
generator.writeString("123");
generator.writeEndObject();
}
}
If you have a plain string you'll get out a plain string when serialised. If you want to wrap it in an object then use a map for the simplest solution.
String value = "123";
Map<String, String> obj = new HashMap<>();
obj.put("value", value);
Passing that through the mapper will produce something like this:
{ "value": "123" }
If you change the map to <String, Object> you can pass in pretty much anything you want, even maps within maps and they'll serialise correctly.
If you really can't have the enclosing curly braces you can always take the substring but that would be a very weird use case if you're still serialising to JSON.
Create a Map:
Map<String, String> map = new HashMap<>();
map.put("value", value);
String parsedValue = ObjectMapper.writeValueAsString(map);
and you will get: {"value":"123"}
If you are using java 8 and want to do it in automated way without creating maps or manually putting string variable name "value", this is the link you need to follow-

How to parse the given string to map of string of map of string of object?

Hi my input string look like this
{
6138249={
value=[multi2, multi3, multi4],
key=TestMulticat
},
6161782={
value=Traps (Bamboo / Box),
key=Observation gear
}
}
I want to map this input string in Map<String,Map<String,Object>> in java.
As the input look more mysterious to me, i am not able to figure out the way to do the same.
I tried ObjectMapper class from jackson but still not able to map. The code i write look like this
Map<String,Map<String,Object>> data=objectMapper.readValue(singledoc, Map.class);
Can somebody suggest me either approach to do this or solution, both will be equally helpful.
Your input doesn't look like valid json, as it has unquoted string values.
Json would look like this:
{
6138249:{
value:["multi2", "multi3", "multi4"],
key:"TestMulticat"
},
6161782:{
value="Traps (Bamboo / Box)",
key="Observation gear"
}
}
For such input you can use jackson:
objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
Map<String, Map<String, Object>> result = objectMapper
.readValue(test, new TypeReference<Map<String, Map<String, Object>>>() {});
ALLOW_UNQUOTED_FIELD_NAMES will help you to deal with unquoted field names, however there is no such option for unqouted values.
In your case as its not a json, you can either fix your serialization to produce valid jackson or write your own Deserializer for jackson handling this, because currently its not possible to read json with unquoted string values.

Parsing dynamic JSON values to Java objects

In my application I have lot of overviews (tables) with sorting and filtering capabilities. And becuase the different column can hold different value type (strings, numbers, dates, sets, etc.) the filter for these columns also can bring different values. Let me show you few examples (converted to JSON already as is sent to server via REST request):
For simple string value it is like:
{"<column_name>":"<value>"}
For number and date column the filter looks like:
{"<column_name>":[{"operator":"eq","value":"<value>"}]}
{"<column_name>":[{"operator":"eq","value":"<value1>"},{"operator":"gt","value":"<value2>"}]}
For set the filter looks like
{"<column_name>":["<value1>","<value2>"(,...)]}
Now I need to parse that JSON within a helper class that will build the WHERE clause of SQL query. In PHP this is not a problem as I can call json_decode and then simply check whether some value is array, string or whatever else... But how to do this simply in Java?
So far I am using Spring's JsonJsonParser (I didn't find any visible difference between different parsers coming with Spring like Jackson, Gson and others).
I was thinking about creating an own data object class with three different constructors or having three data object classes for all of the three possibilities, but yet I have no clue how to deal with the value returned for column_name after the JSON is parsed by parser...
Simply looking on the examples it gives me three possibilities:
Map<String, String>
Map<String, Map<String, String>>
Map<String, String[]>
Any idea or clue?
Jackson's ObjectMapper treeToValue should be able to help you.
http://fasterxml.github.io/jackson-databind/javadoc/2.2.0/com/fasterxml/jackson/databind/ObjectMapper.html#treeToValue%28com.fasterxml.jackson.core.TreeNode,%20java.lang.Class%29
Your main problem is that the first version of you JSON is not the same construction than the two others. Picking the two others you could deserialize your JSON into a Map<String, Map<String, String> as you said but the first version fits a Map.
There are a couple solutions available to you :
You change the JSON format to always match the Map<String, Map<String, String> pattern
You first parse the JSON into a JsonNode, check the type of the value and deserialize the whole thing into the proper Map pattern.
(quick and dirty) You don't change the JSON, but you try with one of the Map patterns, catch JsonProcessingException, then retry with the other Map pattern
You'll have to check the type of the values in runtime. You can work with a Map<String, Object> or with JsonNode.
Map<String, Object>
JsonParser parser = JsonParserFactory.getJsonParser();
Map<String, Object> map = parser.parseMap(str);
Object filterValue = filter.get("<column_name>");
if (filterValue instanceof String) {
// str is like "{\"<column_name>\":\"<value>\"}"
} else if (filterValue instanceof Collection) {
for (Object arrayValue : (Collection<Object>) filterValue) {
if (arrayValue instanceof String) {
// str is like "{\"<column_name>\":[\"<value1>\",\"<value2>\"]}"
} else if (arrayValue instanceof Map) {
// str is like "{\"<column_name>\":[{\"operator\":\"eq\",\"value\":\"<value>\"}]}"
}
}
}
JsonNode
ObjectMapper mapper = new ObjectMapper();
JsonNode filter = mapper.readTree(str);
JsonNode filterValue = filter.get("<column_name>");
if (filterValue.isTextual()) {
// str is like "{\"<column_name>\":\"<value>\"}"
} else if (filterValue.isArray()) {
for (JsonNode arrayValue : filterValue.elements()) {
if (arrayValue.isTextual()) {
// str is like "{\"<column_name>\":[\"<value1>\",\"<value2>\"]}"
} else if (arrayValue.isObject()) {
// str is like "{\"<column_name>\":[{\"operator\":\"eq\",\"value\":\"<value>\"}]}"
}
}
}

Can't parse JSON array of arrays to LinkedHashMap in Jackson

I' m developing an Android REST client. We use JSON as data exchange format, so I use a Jackson parser. I get different Json responses from the server like simple arrays:
{"user_id":"332","user_role":"1"}
or something else. All these stuff I parse to LinkedHashMap<String, Object> and everything works perfectly but when I got this response from the server:
[ { "user_id":"352",
"user_role":"expert",
"name":"Test 12-18",
"description":"Test" },
{ "user_id":"263",
"user_role":"novice lab",
"name":"Tom's Desk",
"description":"Desk"}
]
I got null: {} after parsing.Here is my code where i use Jackson:
ObjectMapper mapParametersToJSON = new ObjectMapper();
String serverResponseBody = responseFromServer.getBody();
LinkedHashMap<String, Object> resultofOperation = new LinkedHashMap<String,
Object>();
TypeReference<LinkedHashMap<String,Object>> genericTypeReferenceInformation = new
TypeReference<LinkedHashMap<String,Object>>() {};
try {
resultofOperation = mapParametersToJSON.readValue(serverResponseBody,
genericTypeReferenceInformation);
So, why Jackson failed to parse this? How can I fix this?
Others have suggested the problem, but solutions are bit incomplete. If you need to deal with JSON Objects and Arrays, you can either bind to java.lang.Object, check the type:
Object stuff = objectMapper.readValue(json, Object.class);
and you will get either List or Map (specifically, ArrayList or LinkedHashMap, by default; these defaults can be changed).
Or you can do JSON trees with JsonNode:
JsonNode root = objectMapper.readTree(json);
if (root.isObject()) { // JSON Object
} else if (root.isArray()) { ...
}
latter is often more convenient.
One nice thing is that you can still create regular POJOs out of these, for example:
if (root.isObject()) {
MyObject ob = objectMapper.treeToValue(MyObject.class);
}
// or with Object, use objectMapper.convertValue(ob, MyObject.class)
so you can even have different handling for different types; go back and forth different representations.
The first JSON in your question is a map, or an object. The second is an array. You're not parsing an array, you're parsing a map.
You need to do something like this:
List<MyClass> myObjects = mapper.readValue(jsonInput, new TypeReference<List<MyClass>>(){});
Almost identical question with answer here.
In JSON the {"key": "value"} is Object and the ["this", "that"] is Array.
So, in case when you're receiving the array of objects you should use something like List<Map<Key, Value>>.
You are facing an error, because [] construction can't be translated into Map reference, only in List or array.
I would recommend do it something in this way:
ObjectMapper objectMapper = new ObjectMapper();
List<Map<String,String>> parsedResult = objectMapper.reader(CollectionType.construct(LinkedList.class, MapType.construct(LinkedHashMap.class, SimpleType.construct(String.class), SimpleType.construct(String.class)))).readValue(serverResponseBody);
//if you need the one result map
Map<String, String> resultMap = new LinkedHashMap<String, String>();
for (Map<String, String> map: parsedResult){
resultMap.putAll(map);
}

Categories