简体   繁体   中英

Gson.toJson returns null [ProGuard issue]

users Bug report shows Gson().toJson(obj) occasionally returns {} but for most users it works correct.

i have visited an user who faced the bug and debugged app on his phone and i made Toast to show what is sending to server and i saw Toast shows {} and also Records and ID aren't null .

here is what i have done.

private class ClassA{
        String ID;
        ArrayList<ClassB> Records;

        ClassA(String ID, ArrayList<ClassB> Records) {
            this.ID= ID;
            this.Records= Records;
        }
 }

 private class ClassB {
        int T;
        int F;
        String D;

        ClassB (int T, int F, String D) {
            this.T= T;
            this.F = F;
            this.D= D;
        }

}

And here i do serialize object

ClassA obj = new ClassA(ID,Records); 
String json = new Gson().toJson(obj);

but new Gson().toJson(obj ) for some users works correct but for some return {}

Server database shows some users sent data {} but some correct json.after some research i found new Gson().toJson(obj ) returns {} .no webservice problem and no database problem.

Extra info

ArrayList<ClassB> Records= new ArrayList<>();
Records.add(new ClassB(Integer.valueOf(t.toString()), Integer.valueOf(f.toString()),d.toString()));
ClassA obj = new ClassA(ID,Records); 
String json = new Gson().toJson(obj);

Database

id    | data
----------------
c89   | {"ID":"c89","Records":[{"T":0,"F":0,"D":"2019/04/11 05:48 PM"}]} correct one

c90   | {} bug

Null Input Test

i did below test and i found problem is beyond the null inputs

ArrayList<ClassB> Records= new ArrayList<>();
ClassA obj = new ClassA(null,Records);    
Toast.makeText(getApplicationContext(),new Gson().toJson(obj ),Toast.LENGTH_LONG).show();

Toast shows {"Records":[]} .worse than upper condition never happens

And also IDE says

if(ID!=null && Records!=null) <= this condition is always true 
ClassA obj = new ClassA(ID,Records); 

proguard-rules.pro

##---------------Begin: proguard configuration for Gson  ----------
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature

# For using GSON @Expose annotation
-keepattributes *Annotation*

# Gson specific classes
-dontwarn sun.misc.**
#-keep class com.google.gson.stream.** { *; }

# Application classes that will be serialized/deserialized over Gson
-keep class com.google.gson.examples.android.model.** { <fields>; }

# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * implements com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer

# Prevent R8 from leaving Data object members always null
-keepclassmembers,allowobfuscation class * {
  @com.google.gson.annotations.SerializedName <fields>;
}

##---------------End: proguard configuration for Gson  ----------

How do i make it to work correct for all users?

Comments

This shouldn't be possible, you need more details on the {} case eg is it consistent, is it due to multi-threading, is it a specific JDK version

There isn't any multi-threading phenomena.for example, no Runnable no AsycTask no Thread .it's just a normal fragment which gets data from content provider and create json string.

Suggested solution has same result !

ClassA obj = new ClassA(ID,Records); 
Gson gson = new GsonBuilder().serializeNulls().create();
Toast.makeText(getActivity(), gson.toJson(obj), Toast.LENGTH_LONG).show();

Toast shows {} again.

This is because ID and Records in ClassA are null. Gson does not serialize nulls by default and actually has a method to enable serializing nulls, which I almost always enable:

private static final Gson GSON = new GsonBuilder().serializeNulls().create();

I recommend keeping a static instance of a Gson around so you don't have to keep creating one each time you want to serialize.

If you don't want to serialize nulls, then you have some other options to fix your code, such as having null checks in the constructor of ClassA .

Just my wild guess, I saw you mentioned in one of the comment that ClassA and ClassB are inner classes inside a fragment. So, maybe there's a few thing you could try, if haven't already.

First: How about changing them into static inner class?

private static class ClassA {
    ...
}

private static class ClassB {
    ...
}

Having classes declared as inner class because no one else use it is okay, I did that sometimes too. But non-static inner class kind of depended on its parent object instance, so declaring them static inner class is much safer, when it's just a simple bean/DTO classes.

Second: Try fiddling around Proguard configuration about those classes

  • Here's a stackoverflow question about setting up Proguard to keep inner classes .

  • Review the -keep class <your packge name>.** { <fields>; } -keep class <your packge name>.** { <fields>; } part

Third: Maybe try extracting those classes into a normal class just to narrow down the suspect area.

If it still doesn't work, then the problem probably lies somewhere else, maybe some specific build/version of the client mobile device or something.

ArrayList<ClassB> Records= new ArrayList<>();
ClassA obj = new ClassA(null,Records);    
Toast.makeText(getApplicationContext(),new Gson().toJson(obj 
),Toast.LENGTH_LONG).show();

In this case you rRecord arraylist is empty not null. try this checking

if(!Records.isEmpty()){ 
 // show toast
}

In your code, there are some interesting things to point out:

  • Why do you have private classes? Is it needed there?
  • Normally, I would always use List (one of the interfaces of ArrayList) as a type to store a list in it. ArrayList would be the specific type of the object passed there in.
  • Variable names always small with camel case.

For your problem: - Gson does not convert null members by default. - Gson does not convert transient members by default.

Use GsonBuilder to customize a Gson instance for your case, for your need.

Thanks @Montri M for nice suggestions:

Second: Try fiddling around Proguard configuration about those classes

Here's a stackoverflow question about setting up Proguard to keep inner classes.

Review the -keep class.** {; } part

Third: Maybe try extracting those classes into a normal class just to narrow down the suspect area.

i created DataModel and extracted inner classes and insert them into DataModel

public class DataModel {

    public static class ClassA{
        String ID;
        ArrayList<ClassB> Records;

        ClassA(String ID, ArrayList<ClassB> Records) {
            this.ID= ID;
            this.Records= Records;
        }
    }

    public static class ClassB {
        int T;
        int F;
        String D;

        ClassB (int T, int F, String D) {
            this.T= T;
            this.F = F;
            this.D= D;
        }

    }
}

And i put these two lines into Proguard-rules.pro .Please do Attention to $ sign

-keep class <package-name>.DataModel.** { *; }
-keep class <package-name>.DataModel$** { *; }

Now i see problem resolved and Gson serializes obj correct.

Are you possibly catching a JsonParseException somewhere? RuntimeTypeAdapterFactory sometimes causes trouble when a type is not registered with the TypeAdapter . Try commandeering the RuntimeTypeAdapterFactory class and in the version that your project uses, replace every occurrence of

if (delegate == null) {
          throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
              + label + "; did you forget to register a subtype?");
        }

With

if (delegate == null) {
    /*
    This class is commandeered from Google/Gson. The edit below was added so that if a type
    is not registered with the TypeAdapter, the app still works instead of throwing an
    exception and crash.
    */
    delegate = (TypeAdapter<R>) labelToDelegate.get(Constants.DEFAULT_TYPE); // "DefaultType" 
}

Try this

GsonBuilder builder = new GsonBuilder().serializeSpecialFloatingPointValues();
 String data = builder.create().toJson(obj);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM