Why does minify cause error "JSONObject["rewards"] not a string"?

1.4k Views Asked by At

Somehow from one App release into another the internals of org.json library broke. It could no longer read the contents of a key as a string. Here is an example of the code that used to work and is breaking when minify/R8/proguard is enabled.

    try {
        parseJson("{\"rewards\":[{\"amount\":1,\"name\":\"Add Audio\"}]}");
    } catch (Exception e) {
        Log.w("TEST", "Error parsing rewarded currencies JSON header!!!", e);
    }
    
    private void parseJson(@NonNull String rewardedCurrencies) throws JSONException {
        final Map<String, String> rewardsMap = jsonStringToMap(rewardedCurrencies);

        for (Map.Entry<String, String> entry : rewardsMap.entrySet()) {
            Log.e("TEST", entry.getKey() + ":" + entry.getValue());
        }
    }

    public Map<String, String> jsonStringToMap(String jsonParams) throws JSONException {
        Map<String, String> jsonMap = new HashMap<String, String>();

        if (TextUtils.isEmpty(jsonParams)) return jsonMap;

        JSONObject jsonObject = (JSONObject) new JSONTokener(jsonParams).nextValue();
        Iterator<String> keys = jsonObject.keys();

        Log.e("TEST", "jsonStringToMap() -> jsonObject: "+jsonObject.toString());

        while (keys.hasNext()) {
            String key = (String) keys.next();
            Log.e("TEST", "jsonStringToMap() -> key: "+key);
            jsonMap.put(key, jsonObject.getString(key));
        }

        return jsonMap;
    }

Log output with failure:

jsonStringToMap() -> jsonObject: {"rewards":[{"amount":1,"name":"Add Audio"}]}
jsonStringToMap() -> key: rewards
Error parsing rewarded currencies JSON header!!!
    j.b.b: JSONObject["rewards"] not a string.
        at j.b.d.getString(SourceFile:4)

So what changed on the project since the last working App release?

targetSdkVersion 28 -> 29

sourceCompatibility JavaVersion.VERSION_1_7 -> JavaVersion.VERSION_1_8

'com.android.tools.build:gradle:4.0.0' -> 'com.android.tools.build:gradle:4.1.1'

distributionUrl -> gradle-6.1.1-all.zip -> gradle-6.5-all.zip

The fix was to apply a new line to the proguard script.

-keep class org.json.** { *; }

And now I'm worried. Why do I need to apply this to proguard? Are there other internal libraries potentially breaking? I can't seem to figure out what has caused this and if it's a potential big issue?

2

There are 2 best solutions below

4
On BEST ANSWER

The only reference I could find to the Exception text in your error log is from here: JSONObject not a String Error

I couldn't find a code-path from the Android org.json implementation that would lead to that Exception. This means that org.json is somehow getting packaged into your App. This would also mean that it's not a standard library problem as you've feared.

Further reinforcing that point: https://github.com/stleary/JSON-java/blob/master/src/main/java/org/json/JSONObject.java#L849-L864

This explains your issue exactly. The implementation of JSONObject#getString(String key) differs to the Android implementation. You can only read String values of an Object this way. Any other value will be rejected and cause the Exception. Since you are reading a JSONArray this will not work.

My example below will fix this issue whether you're using the Android Standard Library or the pulled dependency.

The reason why it works without ProGuard is that Androids org.json is already provided by the System. When minifying, it will rename the dependency org.json package and thus force you to use it.

I would recommend getting rid of the org.json dependency. Alternatively write code that works with both... You may want to switch to Gson or Jackson anyways since it's a lot easier to use.

    public static Map<String, String> jsonStringToMap(String jsonParams) throws JSONException {
        Map<String, String> jsonMap = new HashMap<>();
        if (TextUtils.isEmpty(jsonParams)) return jsonMap;

        // Use JSONObject constructor
        JSONObject jsonObject = new JSONObject(jsonParams);

        Iterator<String> keys = jsonObject.keys();

        Log.e("TEST", "jsonStringToMap() -> jsonObject: " + jsonObject.toString());

        while (keys.hasNext()) {
            String key = keys.next();
            Log.e("TEST", "jsonStringToMap() -> key: " + key);
            // Type safe way to get the String value of any type within the key
            // Probably needs to handle null values correctly
            jsonMap.put(key, String.valueOf(jsonObject.get(key)));
        }

        return jsonMap;
    }
5
On

This issue could happen because when you apply r8 shrinker to your Android app, it renames some of the internal fields and method defined in the class. If these methods/fields are references else where in the code, then it would have undefined/ unexpected behavior. To fix this Try adding this rule in your proguard file and will prevent R8 from renaming publically referenced fields and methods

-keep class org.json.** { *; }