10

I want to serialize a custom Java object, so I can use SharedPreferences to store it and retreive it in another Activity. I don't need persistant storage, the SharedPreferences, I wipe them when my application is closed. I'm currently using GSON for this, but it doesn't seem to work well with Android's SparseArray type.

My objects:

public class PartProfile {

private int gameId;
// Some more primitives
private SparseArray<Part> installedParts = new SparseArray<Part>();

// ...
}

public class Part {
   private String partName;
   // More primitives
}

Serialization:

Type genericType = new TypeToken<PartProfile>() {}.getType();
String serializedProfile = Helpers.serializeWithJSON(installedParts, genericType);
preferences.edit().putString("Parts", serializedProfile).commit();

serializeWithJSON():

public static String serializeWithJSON(Object o, Type genericType) {
    Gson gson = new Gson();
    return gson.toJson(o, genericType);
}

Deserialization:

Type genericType = new TypeToken<PartProfile>() {}.getType();
PartProfile parts = gson.fromJson(preferences.getString("Parts", "PARTS_ERROR"), genericType);

SparseArray<Part> retreivedParts = parts.getInstalledParts();
int key;
for (int i = 0; i < retreivedParts.size(); i++) {
    key = retreivedParts.keyAt(i);
    // Exception here:
    Part part = retreivedParts.get(key);
    // ...
}

Exception:

java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.mypackage.objects.Part

I don't understand why Gson wants to cast a LinkedTreeMap to my object, I never use one in my entire program. I used to have a HashMap<Integer,Part> before I switched to the SparseArray<Part>, and never had issues with that. Are SparseArrays not supported by Gson, or is there an error on my side?

Edit: It seems that the SparseArray gets deserialized correctly, but not the objects inside. Instead of LinkedTreeMaps, these should be of type Part.enter image description here

Lennart
  • 9,657
  • 16
  • 68
  • 84
  • `SharedPreferences` persist across sessions (even if the application is killed). Read [Using Shared Preferences](http://developer.android.com/guide/topics/data/data-storage.html#pref) – Rajesh Jun 21 '13 at 09:35
  • Yeah, but I wipe them manually with clear(). I just wanted to point out that persisting them isn't necessary. – Lennart Jun 21 '13 at 09:38
  • `SharedPreferences` are not meant for sharing data across activities. Please see http://stackoverflow.com/q/3549596/1321873 for more info. – Rajesh Jun 21 '13 at 09:42
  • See if preserving typing information solves your issue: http://pragmateek.com/javajson-mapping-with-gson/#Preserving_type_information – Pragmateek Jun 22 '13 at 17:27

3 Answers3

13

Really there is a way to serialize any kind of SparseArray, here is an example code:

public class SparseArrayTypeAdapter<T> extends TypeAdapter<SparseArray<T>> {

    private final Gson gson = new Gson();
    private final Class<T> classOfT;
    private final Type typeOfSparseArrayOfT = new TypeToken<SparseArray<T>>() {}.getType();
    private final Type typeOfSparseArrayOfObject = new TypeToken<SparseArray<Object>>() {}.getType();

    public SparseArrayTypeAdapter(Class<T> classOfT) {
        this.classOfT = classOfT;
    }

    @Override
    public void write(JsonWriter jsonWriter, SparseArray<T> tSparseArray) throws IOException {
        if (tSparseArray == null) {
            jsonWriter.nullValue();
            return;
        }
        gson.toJson(gson.toJsonTree(tSparseArray, typeOfSparseArrayOfT), jsonWriter);
    }

    @Override
    public SparseArray<T> read(JsonReader jsonReader) throws IOException {
        if (jsonReader.peek() == JsonToken.NULL) {
            jsonReader.nextNull();
            return null;
        }
        SparseArray<Object> temp = gson.fromJson(jsonReader, typeOfSparseArrayOfObject);
        SparseArray<T> result = new SparseArray<T>(temp.size());
        int key;
        JsonElement tElement;
        for (int i = 0; i < temp.size(); i++) {
            key = temp.keyAt(i);
            tElement = gson.toJsonTree(temp.get(key));
            result.put(key, gson.fromJson(tElement, classOfT));
        }
        return result;
    }

}

and to use it you need to register it in your Gson object, like this:

Type sparseArrayType = new TypeToken<SparseArray<MyCustomClass>>() {}.getType();
Gson gson = new GsonBuilder()
    .registerTypeAdapter(sparseArrayType, new SparseArrayTypeAdapter<MyCustomClass>(MyCustomClass.class))
    .create();

you can find this example in this gist.

P.S.: I know it's not optimized at all, but it's only an example to give an idea on how to achieve what you need.

dmarcato
  • 872
  • 8
  • 16
  • 1
    This code works. However, I still do not use it as solution as SparseArray is not Parcable/Serializable. Hence, it is pretty difficult for me to pass it around to different activities and processes. End up, I fall back to HashMap. – Cheok Yan Cheng Oct 22 '13 at 16:48
5

It seems that the SparseArray gets deserialized correctly, but not the objects inside. Instead of LinkedTreeMaps, these should be of type Part.

Your observation is correct, since SparseArray contains Object (not Part), Gson won't have any clue to make Part as your object type. Hence it map your list as its infamous internal type LinkedTreeMap.

To solve it, I think you won't be able to use SparseArray... Or you may try retreivedParts.get(key).toString(), then use gson to parse the object again. But I don't think it's efficient to do that

Hoàng Long
  • 10,746
  • 20
  • 75
  • 124
  • I'm going to accept your answer, since there doesn't seem a proper solution to that. I replaced my SparseArrays with HashMaps, but I had to use `Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create()` to serialize them, since somehow my custom objects weren't properly deserialized without that option enabled. – Lennart Jul 17 '13 at 16:39
0

As pointed out in the other answers SparseArray's internal implementation uses an Object[] to store the values so Gson cannot deserialize it correctly.

This can be solved by creating a custom Gson TypeAdapterFactory:

import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

import android.util.SparseArray;

import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

public class SparseArrayTypeAdapterFactory implements TypeAdapterFactory {
    public static final SparseArrayTypeAdapterFactory INSTANCE = new SparseArrayTypeAdapterFactory();
    
    private SparseArrayTypeAdapterFactory() { }
    
    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        // This factory only supports (de-)serializing SparseArray
        if (type.getRawType() != SparseArray.class) {
            return null;
        }
        
        // Get the type argument for the element type parameter `<E>`
        // Note: Does not support raw SparseArray type (i.e. without type argument)
        Type elementType = ((ParameterizedType) type.getType()).getActualTypeArguments()[0];
        TypeAdapter<?> elementAdapter = gson.getAdapter(TypeToken.get(elementType));
        
        // This is safe because check at the beginning made sure type is SparseArray
        @SuppressWarnings("unchecked")
        TypeAdapter<T> adapter = (TypeAdapter<T>) new SparseArrayTypeAdapter<>(elementAdapter);
        // call nullSafe() to make adapter automatically handle `null` SparseArrays
        return adapter.nullSafe();
    }
    
    private static class SparseArrayTypeAdapter<E> extends TypeAdapter<SparseArray<E>> {
        private final TypeAdapter<E> elementTypeAdapter;
        
        public SparseArrayTypeAdapter(TypeAdapter<E> elementTypeAdapter) {
            this.elementTypeAdapter = elementTypeAdapter;
        }
        
        @Override
        public void write(JsonWriter out, SparseArray<E> sparseArray) throws IOException {
            out.beginObject();
            
            int size = sparseArray.size();
            for (int i = 0; i < size; i++) {
                out.name(Integer.toString(sparseArray.keyAt(i)));
                elementTypeAdapter.write(out, sparseArray.valueAt(i));
            }
            
            out.endObject();
        }

        @Override
        public SparseArray<E> read(JsonReader in) throws IOException {
            in.beginObject();
            
            SparseArray<E> sparseArray = new SparseArray<>();
            while (in.hasNext()) {
                int key = Integer.parseInt(in.nextName());
                E value = elementTypeAdapter.read(in);
                // Use `append(...)` here because SparseArray is serialized in ascending
                // key order so `key` will be > previously added key
                sparseArray.append(key, value);
            }
            
            in.endObject();
            return sparseArray;
        }
        
    }
}

This factory serializes SparseArrays as JSON objects with the key as JSON property name and the value serialized with the respective adapter as JSON value, e.g.:

new SparseArray<List<String>>().put(5, Arrays.asList("Hello", "World"))

    ↓ JSON

{"5": ["Hello", "World"]}

You then use this TypeAdapterFactory by creating your Gson instance using a GsonBuilder on which you register the TypeAdapterFactory:

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory​(SparseArrayTypeAdapterFactory.INSTANCE)
    .create();
Marcono1234
  • 5,856
  • 1
  • 25
  • 43