※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
- "Platform type issue"
- 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
- Your strict naming conventions are a liability
=> 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.JsonReader
com.google.gson.stream.JsonWriter
=>com.squareup.moshi.JsonWriter
- Moshi bonus
JsonReader.Options
JsonReader.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)
- Cache your adapters!
- 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
- Object Mapping without bad leniency
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 serialized
Advanced: 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 }