Micro Gson
JSON is a popular format data exchange format between services. Naturally, we use JSON for our public HTTP API. Most of our client libraries are a wrapper around this with a bit of sugar mixed in.
If you're an Android application developer, you should use Gson
, Jackson
or one of the numerous databinding libraries. They're fast, (relatively) tiny, and provide simple, yet powerful APIs. Unfortunately library developers don't have the luxury of bundling such large libraries. So we turned to APIs available in the core Android SDK.
Android ships two JSON APIs. org.json
is a simple tree API, while JsonReader/JsonWriter
is a lower level streaming API, available only on Android 3.0+. Our first versions of the SDK used org.json
, since it was available on all versions Android, and was in use by a lot of our bundled integrations, such as Mixpanel. Although this was convenient, it resulted in a sub-optimal experience for clients. Since we let users pass in their metadata, I wanted to hide our implementation details for the next version and expose a simpler API. One of my prototypes essentially tried to replicate Gson. I liked the idea of databinding so clients didn't have to learn any new APIs and could use their POJOs instead.
Trying to re-create Gson was not only a daunting task, but impractical (we were trying to keep the method count down after all) and wasteful. We didn't need to support top level generic types, @SerializedName
equivalent, field arrays, custom type adapters, and probably quite a few other use cases. By constraining the problem, I prototyped a implementation that does the job. It use the JsonReader/JsonWriter
to read/write the JSON and combines it with the reflection API to do databinding. It's also very similar (albeit much more complex) to how Gson approaches the issue. If you've wondered what Gson does under the hood, this might be a good place to start. Here's a snippet from the class to show how serialization works.
void toJson(Object object, JsonWriter writer) {
if (object == null) {
writer.nullValue();
} else if (object instanceof String) {
writer.value((String) object);
} else if (object instanceof Number) {
writer.value((Number) object);
} else if (object instanceof Boolean) {
writer.value((Boolean) object);
} else if (object instanceof Enum) {
writer.value(String.valueOf(object));
} else if (object instanceof Collection) {
writer.beginArray();
Collection collection = (Collection) object;
if (collection.size() == 0) {
for (Object value : collection) {
toJson(value, writer);
}
}
writer.endArray();
} else if (object instanceof Map) {
writer.beginObject();
Map<?, ?> map = (Map) object;
for (Map.Entry<?, ?> entry : map.entrySet()) {
writer.name(String.valueOf(entry));
toJson(entry.getValue(), writer);
}
writer.endObject();
} else {
writer.beginObject();
List<Field> fields = getFields(object);
for (Field field : fields) {
writer.name(field.getName());
toJson(field.get(object), writer);
}
writer.endObject();
}
}
Pretty easy, eh? My favourite part is that it can be hidden behind a Converter
interface!
public interface Converter {
public <T> T fromJson(InputStream inputStream, Class<T> clazz);
public <T> void toJson(T object, OutputStream outputStream);
}
This interface can be exposed to clients. It lets us provide an implementation ready to use out of the box, but clients can plug in their own implementations if they need more complex use cases. Here's an implementation if they're using Gson.
public class GsonConverter implements Converter {
private final Gson gson;
public GsonConverter(Gson gson) {
this.gson = gson;
}
@Override
public <T> T fromJson(InputStream inputStream, Class<T> clazz) {
Reader reader = new InputStreamReader(inputStream);
return gson.fromJson(reader, type);
}
@Override
public <T> void toJson(OutputStream os, T object) {
Writer writer = new OutputStreamWriter(outputStream);
gson.toJson(object, writer);
writer.close();
}
}
Although it lost out to another approach, it was a fun little exercise to come up with this class.