※Notes記事では、英語のセッション動画やポッドキャストの内容を(雑に)英語でメモに書き残すことを行っています。本記事は、あくまで動画を見ながら、参考程度に読んでいただくことを想定しています。Notes記事には雑メモ程度のものだったり、書き起こしのようなものもあります。これから実際の動画を見る際には、本記事の内容が少しでもお役に立てば幸いです。(内容において不備、誤字脱字等ありましたら気軽にご連絡いただけると嬉しいです。)
本記事は、droidcon NYC 2017 - Upgrading to Moshi - YouTubeの記事です。
今現在、AndroidにおけるJsonシリアライズ・デシリアライズを行なうライブラリの中では、Gsonが一番人気であると個人的には感じているのですが、 本セッション動画では、Moshiとは何なのか、MoshiがGsonよりも優れている点(またはGsonが提供していてMoshiが現状提供していないAPIについて)、そしてGson→Moshiへの置き換えのメリットや方法などについて紹介されています。(※本セッション動画にはmoshi-kotlinについての説明はほとんどありません。Kotlinに関する話というよりは、MoshiというJsonパーサーライブラリについての説明に重きをおいた発表となっているようです。)

Upgrading to Moshi#
What is Moshi?#
JSON serialization library for Java with a streaming and object-mapping API.
Moshi is kind of “Gson 3”; it is kind of Gson 2 but kind of like “Gson Lite”
It takes great things from Gson API and removes extra part of Gson API that not many people are using
Why update from Gson?#
Gson often contains breaking API changes
Application using Gson does not often update Gson dependnecy
If the code already works with Gson then not necessarily need to upgrade; you are not going to obtain amazing performance optimization by switching from Gson to Moshi
Why update from Gson: Gson#
In Inactive development
Breaking changes
Not many people update to use the latest version
Too lenient
“Platform type issue”
“Date” type adapter
Implementation change in the platform affects your adapter that has relied upon the previous version of the platform implementation
Large API
Inconsistent exceptions(e.g. IOException may occur when it actually should be data exception).
~188KB, 1345 methods (Moshi: 112KB, 759 methods)
Why update from Gson: Moshi optimizations#
Share buffer segments with other Okio users.
- If you use OkHttp or Retrofit or any other libraries that rely on Okio
Avoid allocating strings while deserializing.
JsonReader.selectName(Options)allows you to pre-allocate memories.
How to upgrade?#
FieldNamingPolicy#
Waste of computation time
Bad for code search
Bad for readability
=> Defining model classes as actual JSON response may be clearer. (Even using sneak_cases as wrigin sneak_cases for layout_ids in Android)
Reflective field naming policy#
@SerializedName("the_name")=>@Json(name="the_name")
Streaming API#
It’s the same!
com.google.gson.stream.JsonReader=>com.squareup.moshi.JsonReadercom.google.gson.stream.JsonWriter=>com.squareup.moshi.JsonWriterMoshi bonus
JsonReader.OptionsJsonReader.setFailOnUnknown
JsonReader.Options#
Prepare strings ahead of time:
Options.of("key1", "key2")Read out directly from the input source:
JsonReader.selectName(options), JsonReader.selectString(options)returns index of string in Options.
setFailOnUnknown#
JsonReader.setFailOnUnknown(true)Useful for debugging, not for production app
Fail when
JsonReader.skipValue()is called to ensure you are not missing any JSON data while debugging
Object Mapping#
TypeAdapter => JsonAdapter
No document-level API like
Gson.fromJson()Gson.getAdapter(Type)=>Moshi.adapter(Type)Object Mapping without bad leniency
Platform types require explicitly registered JsonAdapters.
moshi.adapter(java.util.Date.class)moshi.adapter(java.util.ArrayList.class)moshi.adapter(android.graphics.Point.class)
JsonAdapter wrappers:
serializeNulls(),nullSafe(),lenient(),indent(String),failOnUnknown()
TypeToken => com.squareup.moshi.Types factory methods
Moshi preferes plain Java’s java.lang.reflect.Type. => TypeToken.getParameterized(List.class, String.class) => Types.newParameterizedType(List.class, String.class)
Unknown Enums#
enum Exercise { RUN, JUMP, WALK }Gson: exerciseTypeAdapter.fromJson("jog") => returns null
Moshi: exerciseJsonAdapter.fromJson("jog") => throws JsonDataException
EnumWithDefaultValueJsonAdapter => API in Moshi to have fallback enums?
JsonQualifier#
Special-case type qualifiers:
class Data { @JsonAdapter(WrappedStringTypeAdapter.class) String string; }=>
@Retention(RUNTIME) @JsonQualifier @interface WrappedString {}
class Data { @WrappedString String string }WrappedStringTypeAdapter.java
class WrappedStringTypeAdapter extends TypeAdapter<String> {
String read(JsonReader reader) throws IOException {
reader.beginObject();
String string = reader.nextString();
reader.endObject();
return string;
}
}Easier JsonAdapters#
Traditional JsonAdapter.Factory code implementation looks like this:
class PointJsonAdapterFactory implements JsonAdapter.Factory {
JsonAdapter<?> create(Type typem Set<? extends Annotation> annotations, Moshi moshi) {
if (Types.getRawType(types) != Point.class) return null;
return new JsonAdapter<Point> {
Point fromJson(JsonReader reader) { ... }
void toJson(JsonWriter writer) { ... }
}
}
}A lot of boilerplates
The code tends to be error-prone codes
- Often the code is not tested
Blow is the easier version:
class PointJsonAdapter {
@FromJson Point fromJson(JsonReader reader) { ... }
@ToJson void toJson(JsonWriter writer, Point value) { ... }
}It uses reflection API; when you add this object to Moshi.Builder then Moshi will create the factory for you
Here is the even easier ones:
@FromJson Foo fromJson(JsonReader reader)
@FromJson Foo fromJson(JsonReader reader, JsonAdapter<any> delegate, <any more delegates>)
@FromJson Foo fromJson(Bar value) // Bar is already a type that can be deserialized
@ToJson void toJson(JsonWriter writer, Foo value)
@ToJson void toJson(JsonWriter writer, JsonAdapter<any> delegate, <any more delegates>)
@ToJson Bar toJson(Foo value) // Foo is already a type that can be serializedAdvanced: Polymorphic types#
class Animal { String type; }
List<Animal> animals = animalAdapter.fromJson(source)Gson: JsonElement(JsonObject, JsonArray,
Gson: RuntimeTypeAdapterFactory Moshi: ???
Updating piecemeal#
Retrofit#
@Moshi for new code
Retrofit retrofit = new Retrofit.Builder().baseUrl(server.url("/"))
.addConverterFactory(new AnnotatedConverterFactory.Builder()
.add(com.example.Moshi.class, moshiConverterFactory)
.add(com.example.Gson.class, gsonConverterFactory)
.build())
.addConverterFactory(gsonConverterFactory) // Fallback
.build();
interface Service {
@GET("/new_endpoint") @com.example.Moshi Call<Foo> newEndpoint();
@GET("/old_endpoint") @Gson Call<Foo> oldEndpoint();
@GET("/old_endpoint") Call<Foo> oldEndpointDefault(); // Will use Fallback converter
}