Any idea of deserialize mixed json string in gson? - java

I have probably easy question to advanced json/gson users. I get on request something like below:
[{
"1": {
"2": "6+"
}
},{
"1": []
}]
I try deserialize it to java object using gson but I meet problems. Gson reports to me :
java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line X column Y
As I see the problem is that in first item '1' value is declared as an object and in second as and array. I don't have influence on generated JSON. Any idea how map it properly?
Maybe in gson I can add some hook and during parsing have influence what should be done with items? E.g. when for item "1" value is "[]" then do something different than when object with values is given?
After Arkain comment i must add:
In presented example still we have the same object - but it is presented differently :/ (once as empty array - other time as object)
From analysis I think that Object should be represented as e.g.
public class Example {
Map<String, Object> 1 = new Map<String,Object>;
...
}
but i don't know why when map is empty is represented in JSON as an empty array.
I don't know amount of positions and type of particular position in JSON collection.

To fix problem I use answer about custom deserializer from there:
Gson deserialize json with varying value types
I make my own deserializer class where I ignored array types (there are always empty and I do not need them):
public class InternalAttributeDeserializer implements JsonDeserializer<Attributes> {
#Override
public Attributes deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext context) throws JsonParseException {
Attributes attrs = new Attributes();
LinkedTreeMap<String, Object> map = context.deserialize(jsonElement, LinkedTreeMap.class);
for (String key : map.keySet()) {
Object obj = map.get(key);
if (obj instanceof LinkedTreeMap) {
#SuppressWarnings("unchecked")
LinkedTreeMap<String, String> imap = (LinkedTreeMap<String, String>) obj;
for (String ikey : imap.keySet()) {
AttributeInProduct iattr = new AttributeInProduct();
iattr.setPres_id(key);
iattr.setAttribute_id(ikey);
iattr.setValue(imap.get(ikey));
attrs.addAttribute(iattr);
}
}
}
return attrs;
}
}

Related

Object type remains as LinkedHashMap after assignment

I have the following JSON data being fetched from an external file source.
{
.... More complicated structures within this JSON format above but I only care about the following
"feature": {
"100": {
"DATA1": [
{
"header": "head1",
"someId": "123"
}
]
}
}
}
I am trying to capture the following portion as List < Data >
"DATA1": [
{
"header": "head1",
"someId": "123"
}
]
but unable to do so. Getting following error:
Cannot cast LinkedHashMap to List.
Method which fetched the data and try to assign it to List< Data >
private void getData(){
AllDataFromFile all = someFetchAPI(); // Got every JSON data in the file.
// capturing the portion that I want which I do get.
Map<String, Map<String,Object>> feature = all.getFeature();
Map<String, Object> allData = feature.get("100");
List<Data> dataList = allData.get("DATA1");
}
No compilation nor run time errors from above but dataList is not a List of Data.
Instead it is a List of LinkedHashMap when I see in debug mode. Why?
And how can I turn this into a List of Data instead?
Since it doesn't map as a List< Data >, I am unable to perform operations such as follows.
dataList.get(0).getHeader().
Unable to cast it either and getting same error:
Cannot cast LinkedHashMap to List.
Please advice. Thanks.
AllDataFromFile class
#Getter
#Setter
#JsonInclude(Include.NON_NULL)
public class AllDataFromFile {
private Map<String, Map<String,Object>> feature;
}
Data class
#JsonInclude(Include.NON_NULL)
public class Data implements Comparable<Data>, Cloneable{
private String header;
private String someId;
#Override
public int compareTo(Data o) {
int result = // some logic
return result;
}
}
EDIT:
This is data format inside List
result = {ArrayList#18590} size = 1
0 = {LinkedHashMap#18593} size = 2
"header" -> "header1"
"someId" -> "id1"
Getting following error when I try to match the Object's value to be more specific as follows.
private Map<String, Map<String, List<Data>>> feature;
Able to go back to getting LinkedHashMap (no use, back to Square one) if I go for the following:
private Map<String, Map<String, List>> feature;
Error:
Could not read JSON: Class java.util.LinkedHashMap not subtype of
[simple type, class com.a.a.a.a.a.Data] (through reference chain:
com.b.b.b.b.b.b.AllDataFromFile["feature"]->java.util.LinkedHashMap["100"]->java.util.LinkedHashMap["DATA1"]->java.util.ArrayList[0])
Jackson uses reflection to determine what type it needs to deserialize too.
But starting from AllDataFromFile, it cannot determine that the content is actually a List of Data objects, so it falls back to default Lists (ArrayList) and Maps(LinkedHashMap).
I think if you provide more information in the type declaration, jackson might figure it out:
public class AllDataFromFile {
private Map<String, Map<String, List<Data>>> feature;
}

Java - Remove Fields from a JSON String recursively without POJOs

How do I remove some fields with a specified name from a JSON string recursively ?
For example, I want to remove the field "secondName" from the following JSON:
INPUT:
{
"name" : "abc",
"secondName": "qwe",
"add" : "abcd",
"moreDetails" : {
"secondName": "qwe",
"age" : "099"
}
}
OUTPUT:
{
"name" : "abc",
"add" : "abcd",
"moreDetails" : {
"age" : "099"
}
}
I have to remove some fields from a lot of different JSONs with different structures/schema, so I won't be able to deserialize/serialize to/from a POJO.
Gson deserializes any valid Json to LinkedTreeMap, like:
LinkedTreeMap<?,?> ltm = new Gson().fromJson(YOUR_JSON, LinkedTreeMap.class);
Then it is just about making some recursive methods to do the clean up:
public void alterEntry(Entry<?, ?> e) {
if(e.getValue() instanceof Map) {
alterMap((Map<?, ?>) e.getValue());
} else {
if(e.getKey().equals("secondName")) { // hard coded but you see the point
e.setValue(null); // we could remove the whole entry from the map
// but it makes thing more complicated. Setting null
// achieves the same.
}
}
}
public void alterMap(Map<?,?> map) {
map.entrySet().forEach(this::alterEntry);
}
Usage:
alterMap(ltm);
You could try storing the JSON as a JSONObject, iterate over the keys using jsonObject.names() and remove the entries using jsonObject.remove(key).
You can do like below if you know the schema and heirarchy:
JsonObject jsonObj= gson.fromJson(json, JsonObject.class);
jsonObj.getAsJsonObject("moreDetails").remove("secondName");
System.out.println(jsonObj.getAsString());
refer this for more info Remove key from a Json inside a JsonObject
else you need to write a dynamic function which will check each and every element of JSON object and try to find the secondName element in it and remove it.
So consider here as you have multiple nested objects then you need to write a function which will iterate over each element and check its type if its again a jsonObject call the same method recursively or iteratively to check against current element, in each check you need to also verify that the key, if it matches with the key which has to be removed then you can remove it and continue the same.
for a hint on how to check a value type of JSON see this How to check the type of a value from a JSONObject?

What is the reasoning on modelling a class to represent JSON data and do I need to?

I have come across this question on StackOverflow which asks about converting JSON to Java. The answer shows that another class is modelled to represent the JSON data as well as an object being created and I don't understand why.
Does that object now contain all the information after Gson reads the content or only one key/value pair? If it only contains 1 key/value pair, I'm assuming I would need to create multiple objects for the JSON that I have below which I can the use a loop to iterate over and add the values to a drop down menu?
{
"1": "Annie",
"2": "Olaf",
"3": "Galio",
"4": "TwistedFate",
"5": "XinZhao",
"6": "Urgot",
"7": "Leblanc",
"8": "Vladimir",
"9": "FiddleSticks",
"10": "Kayle",
"11": "MasterYi",
"12": "Alistar",
"13": "Ryze",
"14": "Sion",
"15": "Sivir",
"16": "Soraka",
"17": "Teemo",
"18": "Tristana",
"19": "Warwick",
"20": "Nunu"
}
Essentially what I am aiming to do is:
1) Create a list of names with the Values.
2) Sort the list of names (as it comes unsorted) in alphabetical order
3) Loop through the list and add each name to a drop down menu
4) When a name in the drop down menu is selected, the key associated with that value is passed to another url which receives more data.
Sorry if this is unclear. I've spent a couple of hours trying to understand how to get elements from JSON and display it, as well as trying to create a list where I can use the key to display information the name but have had no luck except for using a for-each loop.
Let's use Jackson's feature that allows you to map any property to a single method (you don't really need a getter here I believe). Just swap the key and value in this universal setter, and add to a TreeMap, which is already sorted by key (name). Then you can output the keys (names) in the alphabetical order and get an ID by name easily.
public static void main(String[] args) throws IOException {
String json = "....."; // your JSON string here
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
ReverseMap pairs = mapper.readValue(json, ReverseMap.class);
for (Map.Entry<Object, String> entry : pairs.getValues().entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
public class ReverseMap {
private TreeMap<Object, String> mapping = new TreeMap<>();
#com.fasterxml.jackson.annotation.JsonAnySetter
public void add(String name, Object value) {
mapping.put(value, name);
}
public Map<Object, String> getValues() {
return mapping;
}
}
Gson Bean Mapping Solution
Okay, what you have is a bit unusual for a JSON object; the keys (the numbers in your case) essentially represent properties of their contained object. That's workable, but you have to understand that, for example, when looking for "Annie" in the JSON object, if you use Gson to map to a "bean" class, which we'll call Data (as in the linked example), then you'd have to create a data object like so:
class Data {
private String _1;
// ...
private String _20;
public String get1() { return _1; }
public void set1(String _1) { this._1 = _1; }
// ...
public String get20() { return _20; }
public void set20(String _20) { this._20 = _20; }
}
And by using Data data = new Gson().fromJson(myJsonString, Data.class); on the given string, you'd be able to find "Annie" by calling... uh... data.get1()?
Clearly, this isn't a good solution.
Better Solutions
Since your data doesn't follow the typical format for a JSON object, you have two options:
If you can, refactor your JSON representation to a more verbose, but better representation for parsing.
Use a different approach to parse the existing JSON.
Solution 1: Changing the JSON representation
Refactoring the JSON would result in an object that (preferably) would look like this:
{
"champions" : [
{
"index" : 1,
"name" : "Annie"
},
{
"index" : 2,
"name" : "Olaf"
},
// ...
]
}
This could map easily to a couple of beans that look like this:
class Data {
private List<Champion> champions;
// TODO getters and setters
}
class Champion {
private int index;
private String name;
// TODO getters and setters
}
However, this adds a lot of unnecessary clutter to the JSON object, and isn't really necessary with only two fields per champion (the name, and their index).
You could simplify that further like so:
{
"champions" : [
"Annie",
"Olaf",
// ...
]
}
The bean class for that would then be:
class Data {
private List<String> champions;
// TODO getters and setters
}
Much simpler, but still requires a change to the JSON you're getting, which in some situations isn't possible. If you used this, though, you could also get rid of the "bean" class entirely, via:
List<String> champions = (List<String>) new Gson().fromJson(myJsonString, new TypeToken<List<String>>(){}.getType());
Solution 2: Changing how the JSON is parsed
The arguably better and cleaner solution is just to change how the JSON is parsed.
The goal here (if I understand you correctly) is to parse the JSON and spit out a collection of strings representing each champion's name, accessible by the numeric index of the champion in the JSON representation.
As such, and because of the way the JSON object is laid out as a simple mapping of strings to strings, we can use Gson to pipe directly into a Map<String, Object>, like so:
Map<String, String> mappedValues = new Gson().fromJson(myJsonString, Map.class);
String anniesName = mappedValues.get("1"); // "Annie"
String olafsName = mappedValues.get("2"); // "Olaf"
boolean hasTwentyOneElements = mappedValues.containsKey("21"); // false
This is shorter, requires no "bean" classes, and keeps the original JSON representation. The downside is that you can't easily tell whether the indices of each entry are correct and consistent; ie. if someone types in the wrong number, or deletes one of the entries.
To get a container of all keys, you just use mappedValues.keySet(), and to get a container of all key-value pairs, you use mappedValues.entrySet(), which gives you a Set<Map.Entry<String, String>>. Both of those can be iterated over, and may be in random order (I'm not sure whether the underlying Map implementation preserves insertion order or not).
To get the index for a given name (ie. champ), you'd use something similar to the following:
String index = null;
for (Map.Entry<String, String> entry : mappedValues.entrySet()) {
if (champ.equals(entry.getValue())) {
index = entry.getKey();
break;
}
}
Of course, you'd have to check to see if index is null after this, and handle that appropriately, but it's easily doable.
EDIT: #vempo's answer provides a cleaner, more efficient lookup strategy by means of inverting the map (although the answer is written for Jackson, instead of Gson); an adaptation of this for Gson is as follows (and yes, there is a vastly superior version in java-8, left out for sake of availability):
public Map<String, String> invertMap(Map<String, String> input) {
Map<String, String> newMap = new LinkedTreeMap<String, String>(); // TODO Pick optimal storage class
for (Map.Entry<String, String> entry : input.entrySet()) {
newMap.put(entry.getValue(), entry.getKey());
}
return newMap;
}
// ...
Map<String, String> mappedValues = invertMap(new Gson().fromJson(myJsonString, Map.class));
String annieIndex = mappedValues.get("Annie"); // "1"
String olafIndex = mappedValues.get("Olaf"); // "2"
It's worth noting that this sacrifices efficiency of constructing the map by effectively building it twice (once by Gson and once more to invert), but it makes value lookup much more efficient.

Howto keep types in JSON output with XStream

I'm trying to serialize a Java object to XML and JSON with XStream. For this I'm using a custom converter that uses the HierarchicalStreamWriter. The stream writer only has a setValue() method to add data. When I add data through this method the JSON output will always be a string. How can I use the converter to output integers, boolean and dates i JSON?
So in short, data is displayed as: { "data": "23" }
It should be displayed as : { "data": 23 }
Here is the marshal method in the converter:
#Override
public void marshal(Object o, HierarchicalStreamWriter writer, MarshallingContext mc) {
ArrayList<HashMap<String, Object>> src = (ArrayList<HashMap<String, Object>>) o;
for(HashMap<String, Object> row : src) {
writer.startNode("row");
Iterator<String> it = row.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
writer.startNode(key);
writer.setValue(row.get(key).toString());
writer.endNode();
}
writer.endNode();
}
}
Can anyone help me to resolve this issue?
Clearly you shouldn't be using toString() because you're not giving the JSON anything that isn't a String so it's hardly surprising you get the result you do.
Object o = row.get(key);
if (o instanceof Integer) {
writer.setValue((Integer)o)
} else {
writer.setValue(o.toString())
}
This would now support Integer types and anything else would be a String. Adding in more clauses for all the things you want to support.
Also note you can iterate over a map using the below which should be more convenient:
for(Entry<String, Object) e : map.entrySet())

How to get a java.util.Map from id to string prop in couchdb using ektorp

I'm having trouble dealing with what I thought would be a simple problem. Basically, I need a java.util.Map<String, String>, where ids end up being the map keys, and someField of my document ends up in the values.
I'm really really stuck on this, which greatly surprises me. I've tried writing a separate view:
#View(map="function(d) { if (d.someField) { emit(d.someField, null); } }", name = "someField")
and then use the following Java:
public Map<String, String> getSomeFields() throws JsonParseException, JsonMappingException, IOException {
ViewQuery q = new ViewQuery().designDocId("_design/" + entity.getSimpleName()).viewName("someField");
String result = StreamUtils.inputStreamAsString(db.queryForStream(q), false);
TypeReference<Map<String, String>> mapTypeRef = new TypeReference<Map<String,String>>() {};
// mapper is a Jackson ObjectMapper
return mapper.readValue(result, mapTypeRef);
}
This is already really ugly, but it also doesn't actually work, as it seems the JSON results that queryForStream returns includes random other stuff, rather than just the result of the query. This causes the readValue call to throw an IOException.
I've also tried using reduce to generate a single object containing all these values, but the result of that is that Couch complains the reduce doesn't reduce enough...
I would do something like this:
ViewQuery query = ...
Map<String, String> map = new HashMap<String, String>();
for (ViewResult.Row row : db.queryView(query)) {
map.put(row.getId(), row.getKey());
}
return map;
You will need to pre parse the output from CouchDB as there is no way to avoid returning all of that metadata with the query.
Firstly, your view needs to emit the right data (the object id, and its value).
#View(map="function(d) { if (d.someField) { emit(d.id, d.someField); } }", name = "someField")
The form of the reply is a JSON object String => Object. I would start by mapping the entire reply to this, then selecting the object with the key "rows" which is a JSON Array. Each element in this array is another JSON Object with keys "id", "key", "value". You will then need to map each of these objects to a key value pair in your output.
public Map<String, String> getSomeFields()
throws JsonParseException, JsonMappingException, IOException {
ViewQuery q =
new ViewQuery().designDocId("_design/" +
entity.getSimpleName()).viewName("someField");
String queryRresult =
StreamUtils.inputStreamAsString(db.queryForStream(q), false);
TypeReference<Map<String, Object>> mapTypeRef =
new TypeReference<Map<String,Object>>() {};
TypeReference<List<Map<String,String>>> rowsTypeRef =
new TypeReference<List<Map<String,String>>>() {};
// Map of the top level results which includes the couch meta and the
// rows. We have to use object, because Each value is of a different
// type (string, ints, json objects)
Map<String,Object> topResultMap =
mapper.readValue(queryRresult, mapTypeRef);
// Once we have the top level result, cast the value for key "rows" as
// String, and parse it as a rows type, which is a list of maps.
List<Map<String,String>> rows =
mapper.readValue((String) topResultMap.get("rows"), rowsTypeRef);
// Finally iterator over that list pulling out the id and the value in
// the key and value for the results
Map<String,String> results = new HashMap<String,String>();
for (Map<String,String> row : rows)
results.put(row.get("id"), row.get("value"));
// And return them
return results;
}
Lastly, you need to make sure you don't have a reduce part of your CouchDB view. If you do, you must pass "reduce=false" through to couch.

Categories

Resources