Skip to main content

Notes - droidcon NYC 2017: Upgrading to Moshi

note

This article has been translated by Gemini.

In this Notes article, I roughly jotted down notes in English from the content of the English session video/podcast. This article is intended to be read as a reference while watching the video. Please note that these are just rough notes or transcript-like content. I hope this article helps you even a little when you watch the actual video. (I would be happy if you could let me know if there are any errors or typos.)

This article is based on droidcon NYC 2017 - Upgrading to Moshi - YouTube.

Currently, I personally feel that Gson is the most popular library for JSON serialization/deserialization in Android. However, this session video introduces what Moshi is, why Moshi is superior to Gson (or about APIs that Gson provides but Moshi currently doesn’t), and the merits and methods of replacing Gson with Moshi. (Note: There is almost no explanation about moshi-kotlin in this session video. It seems to focus more on explaining the Moshi JSON parser library itself rather than talking about Kotlin.)

f🆔shaunkawano:20180109131119p:plain

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