heihei blog

Write once, recall anytime. 自分のために書く 📝

Notes - droidcon NYC 2017: Upgrading to Moshi

f:id:shaunkawano:20180109131119p:plain

はじめに

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

=> 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

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

retrofit/AnnotatedConverters.java at e6a7cd01657670807bed24f6f4ed56eb59c9c9ab · square/retrofit · GitHub

@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
}

AutoValue with Moshi

github.com