8

I've inherited some code that saves our application state as JSON using Gson, and then reads it using fromJson.

Gson gson = createGson();
gson.fromJson(objString, myClass);

One of the fields being saved is a Location. Unfortunately, very occasionally the parsing of that saved data fails because my saved Location includes an mClassLoader in its mExtras, and the Gson library fails to create the ClassLoader with this error:

RuntimeException: Failed to invoke protected java.lang.ClassLoader() with no args

Does anybody know why a ClassLoader is being included in the extras for my Location, and whether it should be ending up in the JSON representation?

I'm assuming I can fix this by just saving the key fields from the Location object individually (e.g. longitude, latitude, altitude, time, accuracy), but it would be nice to save out the Location object if possible.

I saw there is an ExclusionStrategy object I could use to exclude fields, but I wasn't sure if I could/should use that to exclude the the extras from inside my Location...

FYI, here's the JSON data for my Location object (with the longitude and latitude changed to hide me):

{
    <snip>
    "lastKnownLocation": {
        "mResults": [
            0,
            0
        ],
        "mProvider": "gps",
        "mExtras": {
            "mParcelledData": {
                "mOwnObject": 1,
                "mObject": 5525040
            },
            "mClassLoader": {
                "packages": {}
            },
            "mMap": {},
            "mHasFds": false,
            "mFdsKnown": true,
            "mAllowFds": true
        },
        "mDistance": 0,
        "mTime": 1354658984849,
        "mAltitude": 5.199999809265137,
        "mLongitude": -122.4376,
        "mLon2": 0,
        "mLon1": 0,
        "mLatitude": 37.7577,
        "mLat1": 0,
        "mLat2": 0,
        "mInitialBearing": 0,
        "mHasSpeed": true,
        "mHasBearing": false,
        "mHasAltitude": true,
        "mHasAccuracy": true,
        "mAccuracy": 16,
        "mSpeed": 0,
        "mBearing": 0
    },
    <snip>
}

Here's an example what the mExtras contains when the code doesn't crash:

"mExtras": {
    "mParcelledData": {
        "mOwnsNativeParcelObject": true,
        "mNativePtr": 1544474480
    },
    "mHasFds": false,
    "mFdsKnown": true,
    "mAllowFds": true
}
Community
  • 1
  • 1
Dan J
  • 25,433
  • 17
  • 100
  • 173

3 Answers3

23

The problem is you're attempting to just straight convert a system-provided class (Location) to JSON. And, as you see you run into problems with serializing internal state / Java specific things. JSON is meant as a semi-generic way to pass information around.

You can't use the @Expose annotation easily because it's not your class; that would require modifying the source code for Location or via a fairly extensive process of adding them at runtime using jassist (see: http://ayoubelabbassi.blogspot.com/2011/01/how-to-add-annotations-at-runtime-to.html)

Looking at the Location class, I'd simply create a custom Gson Serializer and Deserializer and be done with it. What you're actually interested in is the GPS data, not internals of the class itself. You just construct the JSON containing the information you need in the serializer using the getters, then in the Deserializer you create a new instance of Location and populate it with the information from the supplied JSON using the public setters.

class LocationSerializer implements JsonSerializer<Location>
{
    public JsonElement serialize(Location t, Type type, 
                                 JsonSerializationContext jsc)
    {
        JsonObject jo = new JsonObject();
        jo.addProperty("mProvider", t.getProvider());
        jo.addProperty("mAccuracy", t.getAccuracy());
        // etc for all the publicly available getters
        // for the information you're interested in
        // ...
        return jo;
    }

}

class LocationDeserializer implements JsonDeserializer<Location>
{
    public Location deserialize(JsonElement je, Type type, 
                                JsonDeserializationContext jdc)
                           throws JsonParseException
    {
        JsonObject jo = je.getAsJsonObject();
        Location l = new Location(jo.getAsJsonPrimitive("mProvider").getAsString());
        l.setAccuracy(jo.getAsJsonPrimitive("mAccuracy").getAsFloat());
        // etc, getting and setting all the data
        return l;
    }
}

Now in your code you use GsonBuilder and register the classes:

...
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Location.class, new LocationDeserializer());
gsonBuilder.registerTypeAdapter(Location.class, new LocationSerializer());
Gson gson = gsonBuilder.create(); 
...

That should pretty much take care of it.

Dan J
  • 25,433
  • 17
  • 100
  • 173
Brian Roach
  • 76,169
  • 12
  • 136
  • 161
  • Nice answer, thanks. I thought I had checked the Location object implements Serializable, but it turns out it doesn't - so it's no surprise that our code was broken! – Dan J Dec 22 '12 at 00:32
  • Now tested and the fix works great. By saving out the field names with the same names that the system used ("mProvider", "mAccuracy" etc) it is back compatible with old application data containing the mClassLoader extra that used to throw an exception. – Dan J Dec 23 '12 at 01:56
  • Will this approach work if I want to convert List to JSON?? Because jo.getAsJsonPrimitive("objectId") is giving me NullPointerException. – Pranav Mahajan Aug 21 '14 at 07:12
2

Consider to create a custom Object that models your GSON, parsing the content Object per Object and field per field

something like

JSONObject lastKnownLocation = obj.getJSONObject("lastKnownLocation");
JSONArray mResults = lastKnownLocation.getJSONArray("mResults");

etc...
MyGSON mg=new MyGSON(lastKnownLocation, mResults etc....);

So you can obtain the full control of the parsing, and adding a try\catch block in the critical mExtra part you can exclude the block, or manage the exception as you want, easily.

Silverstorm
  • 15,398
  • 2
  • 38
  • 52
0

I guess there's more simple way, but may be it's wrong.

You could delete problematic Extras by:

myClass.loc.setExtras(null);

  • where myClass.loc is a Location.

It does work when I save a Location:

initPrefs(); 
mPrefs.edit().putFloat(PARAM_DISTANCE, mOdometer.getDistance()).apply();
Location loc = mOdometer.getOrigLocation(); 
loc.setExtras(null); 
Gson gson = new Gson(); 
String json = gson.toJson(loc); 
mPrefs.edit().putString(PARAM_ORIG_LOCATION, json).apply();

Without that I got the same error as you - when I was reading the Location.