Friday, May 27, 2011

Ignore missing properties with Json-Lib

Introduction

When you're writing your own client and server protocol, you have to exchange data between them. There are many formats available (csv, xml, serialized objects...) and you can always define your own protocol.
One format that has been getting more popular recently is JSON (JavaScript Object Notation, see http://en.wikipedia.org/wiki/JSON). It's a text format that allows objects to be serialized in a human-readable form. There's a library for Java called Json-lib (http://json-lib.sourceforge.net/) that performs this serialization for you. This allows you to exchange JSON between your server and client without having to write any JSON code.

The Json-lib library

You can find some examples on how to use Json-lib here: http://json-lib.sourceforge.net/snippets.html. I use it like this:
On the server (serialize to JSON):
  User user = ...; //get the user from database
  String json = JSONObject.fromObject(user).toString();
  //Send the json to the client
Then on the client:
   JSONObject jo = JSONObject.fromObject(response);
   User user = (User) JSONObject.toBean(jo, User.class);
As you can see it's pretty straight-forward and it works well, as long as the class definitions (in this case: the User class) are the same on the server and the client. As you can imagine this is not always the case: you will sometimes add new properties to the class while users are still using the old definition of your class on their client. If this is the case (you are sending properties from the server that don't exist on the client class), you get slapped with this exception:
net.sf.json.JSONException: java.lang.NoSuchMethodException: Unknown property on class
You can fix this exception by adding an exclude filter on the server that doesn't send certain properties (see http://json-lib.sourceforge.net/snippets.html for how to do this), but this isn't a solution in our case: we want to send the properties so people who have the newer version of the client can use them.

The solution

After searching for a way of telling Json-lib to ignore missing properties and not finding it, I decided the best approach would be to create my own PropertySetStrategy. A PropertySetStrategy defines how Json-lib sets the properties on your objects. The default strategy works pretty well, except that it throws an exception when a property is not found. So I decided to create a wrapper around this default PropertySetStrategy that ignores any exceptions happening when setting a property.
This is my PropertySetStrategyWrapper:
import net.sf.json.JSONException;
import net.sf.json.util.PropertySetStrategy;

public class PropertyStrategyWrapper extends PropertySetStrategy {

    private PropertySetStrategy original;

    public PropertyStrategyWrapper(PropertySetStrategy original) {
        this.original = original;
    }

    @Override
    public void setProperty(Object o, String string, Object o1) throws JSONException {
        try {
            original.setProperty(o, string, o1);
        } catch (Exception ex) {
            //ignore
        }
    }
}
As you can see I ignore any exceptions that happen on a set. You can always log the exceptions if you prefer.
Now all we need to do is tell Json-lib to use our wrapper. Our client code now looks like this:
JSONObject jo = JSONObject.fromObject(response);
JsonConfig cfg = new JsonConfig();
cfg.setPropertySetStrategy(new PropertyStrategyWrapper(PropertySetStrategy.DEFAULT));
cfg.setRootClass(User.class);
User user = (User) JSONObject.toBean(jo, cfg);
We don't need to modify any of the server code and our client will now happily ignore missing properties!

3 comments:

  1. Thanks for sharing this...I had run into this exact problem trying to deserialize a JSON feed and this was the perfect solution!

    ReplyDelete
  2. that's weird but I can't find any way to use strategy when I need to specify the target class, as in toBean(JSONObject jsonObject, Class beanClass)

    ReplyDelete
    Replies
    1. public static Object toBean(JSONObject jsonObject,
      Object root,
      JsonConfig jsonConfig)

      Delete