7

How can I parse Nested JSON with NESTED dynamic keys in Android kotlin, Moshi and Retrofit?

I get this JSON from alpha-vantage.

Format example:

{
    "Meta Data": {
        "1. Information": "Intraday (15min) open, high, low, close prices and volume",
        "2. Symbol": "AAME",
        "3. Last Refreshed": "2019-11-18 16:00:00",
        "4. Interval": "15min",
        "5. Output Size": "Compact",
        "6. Time Zone": "US/Eastern"
     },
    "Time Series (15min)": {//Dynamic - > Time Series (5min) / Time Series (30min)
        "2019-11-18 16:00:00": {//Dynamic
            "1. open": "1.6700",
            "2. high": "1.6700",
            "3. low": "1.5700",
            "4. close": "1.5700",
            "5. volume": "1521"
        },
        "2019-11-18 15:45:00": {//Dynamic
            "1. open": "1.6600",
            "2. high": "1.7400",
            "3. low": "1.6600",
            "4. close": "1.7400",
            "5. volume": "355"
        }
    }
}

I tried to use custom adapter but I can't find a way to parse a double nested dynamic keys with it. For now I use manual parsing:

fun convertJsonToItemDetails(jso: JSONObject) {
    val meta: JSONObject? = jso.optJSONObject("Meta Data")
    var metaData: ItemMetaData? = null
    meta?.apply {
        val information = optString("1. Information")
        val symbol = optString("2. Symbol")
        val lastRefreshed = optString("3. Last Refreshed")
        val interval = optString("4. Interval")
        val outputSize = optString("5. Output Size")
        val timeZone = optString("6. Time Zone")
        metaData =
            ItemMetaData(information, symbol, lastRefreshed, interval, outputSize, timeZone)
    }
    if (metaData == null) {
        //TODO return error object
        return
    }

    val timeSeriesJSON = jso.optJSONObject("Time Series (${metaData?.interval})")
    val timeSeires = HashMap<String, IntervalOutput>()
    if (timeSeriesJSON == null) {
        //TODO return error object
        return
    }
    for (key in timeSeriesJSON.keys()) {
        val intervalOutPutJSON = timeSeriesJSON.getJSONObject(key)
        val open = intervalOutPutJSON.getString("1. open")
        val high = intervalOutPutJSON.getString("2. high")
        val low = intervalOutPutJSON.getString("3. low")
        val close = intervalOutPutJSON.getString("4. close")
        val volume = intervalOutPutJSON.getString("5. volume")
        timeSeires[key] = IntervalOutput(open, high, low, close, volume)
    }
    val itemDetails = ItemDetails(metaData, timeSeires)
    _itemDetails.value = itemDetails
}
Maor Hadad
  • 1,850
  • 21
  • 36
  • As JSON is the new standard for data interchange these days, aren't there JSON parsing libraries available? – Mikkel Nov 20 '19 at 02:35
  • Yes you are right JSON is the new standard but I didn't find a way to parse this use case with Moshi.@Mikkel – Maor Hadad Nov 20 '19 at 07:21
  • Is there a "for-each" type component to android kotlin? Pseudocode: for each key k in JSONObject JO do X – Patrick Collins Nov 20 '19 at 17:09
  • I don't want to parse it manually (What I did for now), I want to use Moshi for that. – Maor Hadad Nov 20 '19 at 17:38
  • In 'com.fasterxml.jackson.module:jackson-module-kotlin:2.9.2' there's sth like `@JsonRawValue`. But it's not Moshi and therefore just a comment ;-) – longi Dec 06 '19 at 09:23
  • Agree with @longi comment it is pretty easy with com.fasterxml.jackson.module:jackson you don't even need to write custom adapters. – Shripad Jadhav Dec 11 '19 at 07:08
  • 3
    I spent some time trying to come up with a solution for you, but your JSON contains spaces in the key names which ended up breaking Moshi's Kotlin Codegen. I've submitted an issue for this (https://github.com/square/moshi/issues/1052) for this and have decided to leave this problem for now as I don't have the time to write you an adapater factory. – th3ant Dec 11 '19 at 17:07

2 Answers2

2

Use JSONObject keys() to get the Dynamic Keys and then Iterate each particular key to get to the dynamic value.

A very short to print all the JSONObject of 1st Tier is look like this.

var dynamicJSON: String = {
    "Meta Data": {
        "1. Information": "Intraday (15min) open, high, low, close prices and volume",
        "2. Symbol": "AAME",
        "3. Last Refreshed": "2019-11-18 16:00:00",
        "4. Interval": "15min",
        "5. Output Size": "Compact",
        "6. Time Zone": "US/Eastern"
     },
    "Time Series (15min)": {//Dynamic - > Time Series (5min) / Time Series (30min)
        "2019-11-18 16:00:00": {//Dynamic
            "1. open": "1.6700",
            "2. high": "1.6700",
            "3. low": "1.5700",
            "4. close": "1.5700",
            "5. volume": "1521"
        },
        "2019-11-18 15:45:00": {//Dynamic
            "1. open": "1.6600",
            "2. high": "1.7400",
            "3. low": "1.6600",
            "4. close": "1.7400",
            "5. volume": "355"
        }
    }
}  

Then After you have to start parsing it as JSONObject and then further follows the same process for the JSONObject "Meta Data" & "Time Series (15min)"

val dynamicjson: JSONObject = JSONObject(dynamicJSON)
val keys: Iterator<*> = dynamicjson.keys()

while (keys.hasNext()) { // loop to get the dynamic key
   val currentDynamicKey = keys.next() as String
   // get the value of the dynamic key
   val currentDynamicValue: JSONObject = dynamicjson.getJSONObject(currentDynamicKey)
   // do something here with the value... or either make another while loop to Iterate further

   Log.e("JSON Value", currentDynamicValue.toString())
}

The OUTPUT looks like this when I executed the code:

2019-12-12 15:21:08.399 19798-19798/com.animusabhi.dynamicjsonparsing E/JSON Value: {"1. Information":"Intraday (15min) open, high, low, close prices and volume","2. Symbol":"AAME","3. Last Refreshed":"2019-11-18 16:00:00","4. Interval":"15min","5. Output Size":"Compact","6. Time Zone":"US/Eastern"}

2019-12-12 15:21:09.158 19798-19798/com.animusabhi.dynamicjsonparsing E/JSON Value{"2019-11-18 16:00:00":{"1. open":"1.6700","2. high":"1.6700","3. low":"1.5700","4. close":"1.5700","5. volume":"1521"},"2019-11-18 15:45:00":{"1. open":"1.6600","2. high":"1.7400","3. low":"1.6600","4. close":"1.7400","5. volume":"355"}}

check and let me know if it not works.

Community
  • 1
  • 1
Abhinav Suman
  • 940
  • 1
  • 9
  • 29
0

Just pass your json string to this method, it will work definitely.

private void parseJson(String jsonString) {
    try {
        //  pass your response json string here
        JSONObject object = new JSONObject(jsonString);

        Iterator<String> it = object.keys();
        while (it.hasNext()) {
            String key = it.next();
            try {
                if (object.get(key) instanceof JSONObject) {
                    Log.e("Main OBJ", key);
                    JSONObject object2 = object.getJSONObject(key);
                    Iterator<String> it2 = object2.keys();
                    while (it2.hasNext()) {
                        String key2 = it2.next();
                        if (object2.get(key2) instanceof JSONObject) {
                            Log.e("Sub OBJ", key2);
                            JSONObject object3 = object2.getJSONObject(key2);
                            Iterator<String> it3 = object3.keys();
                            while (it3.hasNext()) {
                                String key4 = it3.next();
                                Log.e("Values", key4);
                            }
                        } else {
                            Log.e("Values", object2.getString(key2));
                        }
                    }

                } else {
                    System.out.println(key + ":" + object.getString(key));
                }
            } catch (Throwable e) {
                try {
                    System.out.println(key + ":" + object.getString(key));
                } catch (Exception ee) {
                    ee.printStackTrace();
                }
                e.printStackTrace();
            }
        }

    } catch (JSONException e) {
        e.printStackTrace();
    }
}

Output:

Main OBJ: Meta Data
Values: Intraday (15min) open, high, low, close prices and volume
Values: AAME
Values: 2019-11-18 16:00:00
Values: 15min
Values: Compact
Values: US/Eastern
Main OBJ: Time Series (15min)
Sub OBJ: 2019-11-18 16:00:00
Values: 1. open
Values: 2. high
Values: 3. low
Values: 4. close
Values: 5. volume
Sub OBJ: 2019-11-18 15:45:00
Values: 1. open
Values: 2. high
Values: 3. low
Values: 4. close
Values: 5. volume
Gundu Bandgar
  • 2,593
  • 1
  • 17
  • 21
  • It's not answering my question. It selected by default cause the bounty period ended. – Maor Hadad Dec 14 '19 at 20:01
  • is this not correct answer? I think you canot use json parsing library, because you need to specify all keys first, if any key change they directly throw in error response, and your response keys are dynamic. – Gundu Bandgar Dec 16 '19 at 05:16
  • 2
    It's not the correct answer. You can parse a dynamic key with a Map. The problem here is the nested dynamic keys. Also, like other people here said it's possible with other libraries(I didn't check it). – Maor Hadad Dec 16 '19 at 07:13