Log in



Serialization 101 with JSON in Android

Sunday, June 17th, 2012 by

JSON is a wonderful data format. It is lightweight, easy to use, and has a simple structure that closely matches data types and structures available in any full-featured programming language (implementations exist for several dozen programming languages). When i am not using libs11n for serialization, i use JSON.

This post is about getting started with JSON-based serialization in Java. What does “serialization” mean? It basically means “saving and loading,” but at a more abstract level than simply to/from files. Serialization has many uses, e.g. passing data between activities or services (e.g. GDrive or the clipboard). Why not use Java’s built-in serialization? It is fine for RPC and other forms of transient data, but its format is not guaranteed to stay stable across invocations, and is therefore completely unsuited for use in storing application data longer-term.

Forewarning: the JSON API used in this article is based on the same json.org API built in to Android (they have a stripped-down version), but this copy is slightly modified from the original (i forked it a couple years ago from json.org for work unrelated to Android). i.e. it behaves slightly differently than the Android JSON API. The most notable difference is that after using the API in several applications i changed the checked JSONException to an unchecked exception. This greatly simplifies use for the majority of cases (where we only work with internal data which “cannot fail” if we’ve done our jobs properly) while still allowing the client to explicitly catch errors if, e.g., they are trying to deserialize user-provided data (which we don’t know in advance will validate as JSON). It also has some (small) support for Java 6 Generics, where the json.org API does not (they want theirs to be portable to older JVMs).

Step one: Create an Interface

Let’s define a simple interface for classes which are to be serializable via JSON. We do not need to do this, and i often do not do this when serializing containers or simple types like instances of the Paint class. Nonetheless, it’s a nice formality to have and often becomes more useful as a project grows.

/**
 * An interface for JSON-able classes.
 */
public interface JSONable {
    /**
     * Must return a JSON-compliant representation of this object,
     * in the form of a JSONObject.
     *
     * @return The JSONObject representation.
     * @throws JSONException if any of the underlying JSONObject members throws.
     *  Implementations may throw other unchecked exceptions.
     */
    JSONObject toJSONObject();

    /**
     * Must populate this object from the given JSON source.
     *
     * @param src The source JSON data.
     * @throws JSONException If any JSONObject members throw. Implementations
     * may optionally throw if the input object violates the expected structure.
     */
    void fromJSONObject( final JSONObject src );
}

 

That’s pretty straightforward, but i will make a side note about fromJSONObject(): implementors must make a couple subtle yet important design decision in that function: do they completely clear out any existing object state and re-populate solely from the JSON input (which might be missing certain properties), or do they only replace properties which are in the JSON input? If JSON is missing certain properties, do they use some specified default or do they re-use the currently-set value for that property? Should the function throw if a specific property is missing? There is no generic correct answer. Philosophically speaking, completely wiping out the current state before restoring it from JSON is “the righter thing” to do, but in some use cases it makes perfectly good sense to simply recycle existing property values if the input JSON does not specify them. A pattern i often use is to call (new T()), then (myT.fromJSONObject(…)), rather than deserializing an existing object “in-place,” and this provides the same effect as the clean-before-deserialization approach.

As a rule of thumb, when i serialize app-internal data, i tend to be pretty lax and take the “use existing values as defaults” approach, and if deserialization of a small/unimportant piece of data fails then i tend to ignore the error and recover with a default or recycle the current value. When deserializing user-provided data we generally have to be a bit more careful. Since Android protects an app’s private files from tampering, it is “normally pretty safe to assume” (famous last words) that data an app saves for itself will deserialize properly.

JSON aficionados might notice that the interface does not account for Array data. That is true – i never use arrays as top-level/root JSON objects. Arrays are indirectly supported via adding them as JSONObject properties, using JSONArray as the value type.

 

Step 2: Make something Serializable

We need a class to make serializable. Rather than contrive one for demonstration purpose, we’ll re-use one which is part of HGR. HGR’s largest pieces of serialized data are android.graphics.Paint objects (they account for probably 90% of its storage requirements). With just a little bit of effort, we can save and load them via JSON…

/**
 Serialization helper for android.graphics.Paint objects.
 */
public final class PaintSerializer
    /* we don't call this class Paint b/c the name collisions would be a headache */ {
    /**
     Serializes p to a new JSONObject and returns it.

     @param p Input object to serialize. Must not be null.
     @return Serialized state of p.
     */
    public static JSONObject toJSONObject(final Paint p){
        JSONObject jo = new JSONObject();
        jo.put("color", p.getColor());
        jo.put("strokeWidth", p.getStrokeWidth());
        jo.put("flags", p.getFlags());
        jo.put("alpha", p.getAlpha());
        jo.put("style", p.getStyle().toString() );
        return jo;
    }

    private final static Pattern PATTERN_INTEGER = Pattern.compile("^\\d+$");
    /**
     Deserializes p from jo. Returns false if jo "does not appear" to have been
     serialized by toJSONObject() (and does not modify p in that case), else
     it returns true.

     @param jo Source object in the format produced by toJSONObject()
     @param p Target object to deserialize the state to.
     @return True if deserialization at least partly worked, else false.
     */
    public static boolean fromJSONObject( final Paint p, final JSONObject jo){
        String s = jo.optString("color", null);
        if(null == s) return false;
        else { // try integer and #RRGGBB
            final Matcher match = PATTERN_INTEGER.matcher( s );
            p.setColor( match.find() ? Integer.parseInt(s) : Color.parseColor(s));
        }
        double d = jo.optDouble("strokeWidth", p.getStrokeWidth() );
        p.setStrokeWidth( (float)d );
        int i = jo.optInt("flags", p.getFlags());
        p.setFlags( i );
        i = jo.optInt("alpha", p.getAlpha());
        p.setAlpha( i );
        s = jo.optString("style",null);
        if(null!=s) {
            try {
                p.setStyle(Paint.Style.valueOf(s));
            }catch(Exception e){/* ignore*/}
        }
        return true;
    }
}

Note that this class does not implement the JSONable interface we so carefully crafted! Why not? An accident of evolution. When serializing small objects of some built-in type i normally use static helpers rather than implement a class which simply wraps the original handle for the purposes of one serialization or deserialization call. That said, the interface of this class is abstractly identical to the JSONable interface, except that the “this” object is passed as the first parameter.

(Trivia: the above code is missing the parts which handle the Paint objects’ Typeface member. That is left as an exercise to the reader (there are several valid approaches).)

Step 3: Serialize It

Normally (but not always) we need to save data before we can load it (though sometimes read-in data is not saved by our app). So here’s how we do it:

Paint p = new Paint();
p.setStrokeWidth( 3 );
p.setFlags( Paint.ANTI_ALIAS_FLAG ); 
...
final String json = PaintSerializer.toJSONObject().toString();

That’s all there is to it. We can now save it anywhere it’s legal to save a string.

Step 4: Deserialize It

This is equally simple, and can be made even simpler by adding type-specific wrappers:

JSONObject jo = new JSONObject( inputJSONString );
Paint p = new Paint();
PaintSerializer.fromJSONObject( p, jo );

Again, that’s all there is to it. Be aware that the JSONObject constructor can throw an unchecked JSONException, and it is a very good idea to try/catch that, especially when reading user-provided data. Failing to do so will lead to a force-close of the app unless a higher-level caller catches it.

Step 5: Go Forth and Be Serializable

Once a type is serializable, it’s usable in any serializable context which supports that serialization format (in our case JSON). What does that mean? It means that we can now de/serialize a Map<String,Paint> or a Map<Integer,List<Paint>>, or equally esoteric combinations, from/to JSON with equal ease. It helps if the types being serialized have a common interface, as opposed to a static helper like the PaintSerializer shown above, because we can then write generic algorithms to assist us. It is often useful to create a utility class which wraps up common JSON-related functionality, e.g. saving and loading JSON to/from app resources/assets, streams, or files.

In closing

To help the newly-initiated on their way, here are a couple of generic routines i find helpful for handling my app-private JSON files:

/**
 * Reads the entire contents of the given input stream and returns it as a string.
 * This function does not do any sort of parsing of the data.
 *
 * @param is The input stream to consume.
 * @return The entire contents of the stream.
 * @throws IOException If the reading API throws.
 */
public static String readAll( final InputStream is ) throws IOException {
    if( null == is ) {
        throw new IllegalArgumentException(JSONUtil.class.getName()+".readAll() was passed a null stream!");
    }
    StringBuilder sb = new StringBuilder();
    {
        /**
         WTF does the StringBuilder API not support byte
         or byte[] args? That lack makes populating it from an
         input stream a pain in the butt (and inefficient - with
         byte[] we could buffer a number of bytes per read()).
         */
        int rc = 0;
        while( (rc = is.read()) &gt;= 0 )
        {
            sb.append( (char) rc );
        }
    }
    return sb.toString();
}

public static JSONObject loadAppJSONFile( Context appContext, final String baseName ){
    Exception ex = null;
    try{
        FileInputStream fis = appContext.openFileInput(baseName);
        JSONObject jo = new JSONObject( readAll(fis) );
        fis.close();
        return jo;
    }
    catch(JSONException e){
        return /*ignore*/ null;
    }
    catch(FileNotFoundException e){
        return /*ignore*/ null;
    }
    catch(IOException e){
        return /*ignore*/ null;
    }
}

public static boolean saveAppJSONFile( Context appContext, final String baseName, final String json ){
    try{
        final FileOutputStream fos = appContext.openFileOutput( baseName, Context.MODE_PRIVATE);
        fos.write( json.getBytes() );
        fos.close();
        return true;
    }
    catch(IOException e){
        Log.e(JSONUtil.class.getName(), "EXCEPTION in saveAppJSONFile(" + baseName + "): " + e, e);
        return false;
    }
}

public static boolean saveAppJSONFile( Context appContext, final String baseName, final JSONObject jo ){
    try{
        return saveAppJSONFile( appContext, baseName, jo.toString() );
    }
    catch( JSONException e ){
        Log.e(JSONUtil.class.getName(), "EXCEPTION in saveAppJSONFile(" + baseName + "): " + e, e);
        return false;
    }
}

 

Happy Hacking!

—– stephan beal

Comments are closed.