Trello Android has been converted from recent use. jison to linen To process JSON. It was a bit tricky, so I wanted to document the process.
(For context, Trello Android primarily parses JSON. It seldom serializes JSON, so the focus here is on deserialization.)
The three main reasons for the switch from hand to Moshi are: safety, speed, and bad life choices.
safety – Gson doesn’t understand Kotlin’s null safety and puts null values into non-null properties. Also, defaults sometimes work (depending on constructor settings).
speed – Many benchmarks (One, 2, three) Moshi is generally faster than Gson. Settings after conversion some benchmarks In our app, we saw a 2x to 3.5x speedup compared to real parsing.
bad life choices – Instead of using Gson to parse JSON into a simple model, write a sophisticated, confusing and brittle custom deserializer with too much logic. The refactoring gave us a chance to fix this architectural error.
Why did you choose Moshi over your competitors, e.g. Kotlin serialization), we generally trust Square’s library, have used Moshi for projects (both at work and at home) in the past, and feel it works well. We haven’t done an in-depth study of the alternatives.
The first step was to make sure we could switch between the old Gson implementation and the new Moshi implementation using feature flags. i wrote JsonInterop
A class to parse all JSON responses using Gson or Moshi based on flags.
(I’ve decided not to use tools like: moshi-gson-interop Because I wanted to test that Moshi parsing is fully functional. If you’d rather mix Moshi and Moshi at the same time, that library would be useful.)
Gson gives you the opportunity to override the default name of the key with: @SerializedName
. With Moshi, you can do the same. @Json
. It’s all great and nice, but it seemed really easy to make a mistake here in Gson vs. Moshi where properties are parsed into different names.
So I wrote some unit tests to make sure the generated Moshi adapter has the same result as Gson’s parsing. In particular, I tested…
- …Moshi can generate an adapter for each class you want to deserialize (not necessarily the correct adapter!). (Otherwise Moshi will throw an exception.)
- …with each field annotated
@SerializedName
It was also annotated with@Json
(using the same key).
Between these two checks, it was easy to find when I made a mistake in updating the class in a later step.
(You can’t include sources here, but by default ClassPath in Guava Collect all classes, then search for issues.)
Gson allows you to parse a plain JSON tree using: JsonElement (and friends). I’ve found this useful in some contexts, such as parsing socket updates (unless you know exactly how to parse the response model until the end of the initial processing).
Obviously Moshi won’t be happy with using Gson’s classes, so Map<String, Any?>
(and sometimes List<Map<String, Any?>>
) for a plain tree of data. Both Gson and Moshi can parse:
fun <T> fromJson(map: Map<String, Any?>?, clz: Class<T>): T? {
return if (USE_MOSHI) {
moshi.adapter(clz).fromJsonValue(map)
}
else {
gson.fromJson(gson.toJsonTree(map), clz)
}
}
Also, Gson is friendly with parsing via: ReaderBut Moshi is not like that. i use it Buffered Source It was a good alternative as it can be converted to a reader of older Gson code.
The easiest adapter for your Moshi is the one you just tap on. @JsonClass
Call them a day. Unfortunately, as mentioned earlier, the Gson parser had a lot of unfortunate custom deserialization logic.
it’s pretty easy Writing custom Moshi adapters, but because it was there Too The custom logic of the deserializer is not cut off by just writing a single adapter. We had to create an interstitial model to parse the raw JSON and then apply it to the model we use.
As a concrete example, imagine we have. data class Foo(val count: Int)
, but the actual JSON we return is of the form:
{
"data": {
"count": 5
}
}
Gson allows you to manually view the tree and get it from the count. data
But we found that there was madness in the way. We want to parse using a simple POJO, but still want to output Foo at the end (so we don’t have to change the whole codebase).
To solve this, I create a new model and use it in a custom adapter like this:
@JsonClass(generateAdapter = true) data class JsonFoo(val data: JsonData)
@JsonClass(generateAdapter = true) data class JsonData(val count: Int)
object FooAdapter {
@FromJson
fun fromJson(json: JsonFoo): Foo {
return Foo(count = json.data.count)
}
}
Voila! Now the parser can still output Foo, but we are using a simple POJO to model the data. Easy to interpret and easy to test.
Remember Gson said that we would happily parse null values into non-null models? We were (sadly) resorting to this behavior in all kinds of places. In particular, sockets in Trello often return partial models. So, you would normally expect a card with a name to be returned, but in some cases this may not be the case.
That meant we had to monitor collisions for when Moshi was blown up (due to a null value) when Gson rejoiced like a clam. This is where the feature flags really shine. Because there is no need to push buggy parsers to naive production users!
After fixing dozens of these bugs protocol buffer. There are many bugs that would not have occurred if there had been a contract between the server and the client.