Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Say "hi" to Moshi (Android Budapest meetup - January 2020)

Márton Braun
January 30, 2020

Say "hi" to Moshi (Android Budapest meetup - January 2020)

JSON serialization is an everyday task for Android developers, but it's still not a trivial one. Long used solutions such as Gson have serious pain points, especially when using them with Kotlin. Could Moshi be our saviour when it comes to dealing with JSON on Android? We'll look at how to use it, what problems it solves, and how nicely it plays with our favourite JVM language.

Talk recording: https://www.youtube.com/watch?v=qadYVYc-8mc

Márton Braun

January 30, 2020
Tweet

More Decks by Márton Braun

Other Decks in Technology

Transcript

  1. Gson 101 val gson = Gson() val burger: Burger =

    Gson().fromJson("{...}", Burger::class.java) val json: String = Gson().toJson(Burger()) @zsmb13
  2. Gson 101 val gson = Gson() val burger: Burger =

    gson.fromJson("{...}", Burger::class.java) val json: String = gson.toJson(Burger()) @zsmb13
  3. Gson val gson = Gson() val burger: Burger = gson.fromJson("{...}",

    Burger::class.java) val json: String = gson.toJson(Burger()) 101 @zsmb13
  4. Gson val gson = Gson() gson.toJson(Date()) "Jan 30, 2020 7:03:50

    PM" gson.toJson(Calendar.getInstance()) : built-in types @zsmb13
  5. Gson val gson = Gson() gson.toJson(Date()) "Jan 30, 2020 7:03:50

    PM" gson.toJson(Calendar.getInstance()) {"year":2020,"month":0,"dayOfMonth":30,"hourOfDay":19, "minute":03,"second":50} : built-in types @zsmb13
  6. Gson val gson = Gson() gson.toJson(Date()) "Jan 30, 2020 7:03:50

    PM" gson.toJson(Calendar.getInstance()) {"year":2020,"month":0,"dayOfMonth":30,"hourOfDay":19, "minute":03,"second":50} gson.toJson(Random()) : built-in types @zsmb13
  7. Gson val gson = Gson() gson.toJson(Date()) "Jan 30, 2020 7:03:50

    PM" gson.toJson(Calendar.getInstance()) {"year":2020,"month":0,"dayOfMonth":30,"hourOfDay":19, "minute":03,"second":50} gson.toJson(Random()) {"seed":{"value":137564286790437},"nextNextGaussian":0.0, "haveNextNextGaussian":false} : built-in types @zsmb13
  8. val gson = GsonBuilder() .setFieldNamingPolicy( FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create() Gson: field naming

    data class Burger( val name: String, val totalCost: Int, val salesPrice: Int ) @zsmb13
  9. val gson = GsonBuilder() .setFieldNamingPolicy( FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create() Gson: field naming

    data class Burger( val name: String, val totalCost: Int, val salesPrice: Int ) gson.toJson(Burger("Classic", 10, 13)) {"name":"Classic","total_cost":10,"sales_price":13} @zsmb13
  10. @SerializedName("name") @SerializedName("total_cost") @SerializedName("sales_price") Gson() Gson: field naming gson.toJson(Burger("Classic", 10, 13))

    {"name":"Classic","total_cost":10,"sales_price":13} data class Burger( val gson = val name: String, val totalCost: Int, ) val salesPrice: Int @zsmb13
  11. data class Burger( val name: String, val price: Int )

    Gson: missing data Gson() val gson = @zsmb13
  12. data class Burger( val name: String, val price: Int )

    Gson: missing data val burger = gson.fromJson("{\"name\": \"Cheeseburger\"}", Burger::class.java) Gson() val gson = @zsmb13
  13. data class Burger( val name: String, val price: Int )

    Gson: missing data val burger = gson.fromJson("{\"name\": \"Cheeseburger\"}", Burger::class.java) Burger(name=Cheeseburger, price=0) Gson() val gson = @zsmb13
  14. data class Burger( val name: String, val description: String )

    val gson = Gson() Gson: missing data val burger = gson.fromJson("{\"name\": \"Cheeseburger\"}", Burger::class.java) @zsmb13
  15. data class Burger( val name: String, val description: String )

    val gson = Gson() Gson: missing data val burger = gson.fromJson("{\"name\": \"Cheeseburger\"}", Burger::class.java) Burger(name=Cheeseburger, description=null) @zsmb13
  16. data class Burger( val name: String, val description: String =

    "One of our best burgers!" ) val gson = Gson() Gson: missing data val burger = gson.fromJson("{\"name\": \"Cheeseburger\"}", Burger::class.java) @zsmb13
  17. data class Burger( val name: String, val description: String =

    "One of our best burgers!" ) val gson = Gson() Gson: missing data val burger = gson.fromJson("{\"name\": \"Cheeseburger\"}", Burger::class.java) Burger(name=Cheeseburger, description=null) @zsmb13
  18. val gson = Gson() Gson: malformed data data class Burger(val

    name: String, val price: Int) data class Restaurant(val menu: List<Burger>) @zsmb13
  19. val gson = Gson() Gson: malformed data data class Burger(val

    name: String, val price: Int) data class Restaurant(val menu: List<Burger>) val json = """{ "menu": [ { "name": "Classic", "price": 10 }, { "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "crocodile" } ] }""" @zsmb13
  20. val gson = Gson() Gson: malformed data data class Burger(val

    name: String, val price: Int) data class Restaurant(val menu: List<Burger>) val json = """{ "menu": [ { "name": "Classic", "price": 10 }, { "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "crocodile" } ] }""" @zsmb13
  21. val gson = Gson() Gson: malformed data data class Burger(val

    name: String, val price: Int) data class Restaurant(val menu: List<Burger>) val json = """{ "menu": [ { "name": "Classic", "price": 10 }, { "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "crocodile" } ] }""" val restaurant = gson.fromJson(json, Restaurant::class.java) @zsmb13
  22. Gson: malformed data val gson = Gson() data class Burger(val

    name: String, val price: Int) data class Restaurant(val menu: List<Burger>) val json = """{ "menu": [ { "name": "Classic", "price": 10 }, { "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "crocodile" } ] }""" val restaurant = gson.fromJson(json, Restaurant::class.java) Exception in thread "main" com.google.gson.JsonSyntaxException: java.lang.NumberFormatException: For input string: "crocodile" @zsmb13
  23. val gson = Gson() Gson: malformed data data class Burger(val

    name: String, val price: Int) data class Restaurant(val menu: List<Burger>) val json = """{ "menu": [ { "name": "Classic", "price": 10 }, { "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "" } ] }""" val restaurant = gson.fromJson(json, Restaurant::class.java) Exception in thread "main" com.google.gson.JsonSyntaxException: java.lang.NumberFormatException: For input string: "" @zsmb13
  24. public JavaBurger(String name, int price) { this.name = name; this.price

    = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } Moshi 101 } public class JavaBurger { private String name; private int price; @zsmb13
  25. // ... public JavaBurger(String name, int price) { this.name =

    name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } Moshi 101 } public class JavaBurger { private String name; private int price; @zsmb13
  26. // ... public JavaBurger(String name, int price) { this.name =

    name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } Moshi 101 val moshi = Moshi.Builder().build() } public class JavaBurger { private String name; private int price; @zsmb13
  27. // ... public JavaBurger(String name, int price) { this.name =

    name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } Moshi 101 val moshi = Moshi.Builder().build() val json = moshi.toJson(JavaBurger("Classic", 10)) } public class JavaBurger { private String name; private int price; @zsmb13
  28. // ... Moshi 101 val moshi = Moshi.Builder().build() val json

    = moshi.toJson(JavaBurger("Classic", 10)) } public class JavaBurger { private String name; private int price; @zsmb13
  29. // ... Moshi 101 val moshi = Moshi.Builder().build() val json

    = moshi.toJson(JavaBurger("Classic", 10)) } public class JavaBurger { private String name; private int price; val burgerAdapter: JsonAdapter<JavaBurger> = moshi.adapter(JavaBurger::class.java) @zsmb13
  30. // ... Moshi 101 val moshi = Moshi.Builder().build() } public

    class JavaBurger { private String name; private int price; val burgerAdapter: JsonAdapter<JavaBurger> = moshi.adapter(JavaBurger::class.java) val json = burgerAdapter.toJson(JavaBurger("Classic", 10)) @zsmb13
  31. // ... Moshi 101 val moshi = Moshi.Builder().build() } public

    class JavaBurger { private String name; private int price; val burgerAdapter: JsonAdapter<JavaBurger> = moshi.adapter(JavaBurger::class.java) val json = burgerAdapter.toJson(JavaBurger("Classic", 10)) {"name":"Classic","price":10} @zsmb13
  32. // ... Moshi 101 val moshi = Moshi.Builder().build() } public

    class JavaBurger { private String name; private int price; val burgerAdapter: JsonAdapter<JavaBurger> = moshi.adapter(JavaBurger::class.java) val json = burgerAdapter.toJson(JavaBurger("Classic", 10)) {"name":"Classic","price":10} val burger = burgerAdapter.fromJson(json) @zsmb13
  33. Burger::class.java) val moshi = Moshi.Builder().build() (val : String, val :

    Int) val burgerAdapter: JsonAdapter<Burger> = moshi.adapter( Burger class name price @zsmb13 Moshi ft. Kotlin
  34. Burger::class.java) val moshi = Moshi.Builder().build() (val : String, val :

    Int) val burgerAdapter: JsonAdapter<Burger> = moshi.adapter( Burger class name price @zsmb13 Moshi ft. Kotlin Exception in thread "main" java.lang.IllegalArgumentException: Cannot serialize Kotlin type com.example.Burger. Reflective serialization of Kotlin classes without using kotlin-reflect has undefined and unexpected behavior. Please use KotlinJsonAdapter from the moshi-kotlin artifact or use code gen from the moshi-kotlin- codegen artifact.
  35. Moshi ft. Kotlin val moshi = Moshi.Builder().build() val burgerAdapter: JsonAdapter<Burger>

    = moshi.adapter(Burger::class.java) class Burger(val name: String, val price: Int) @zsmb13 Exception in thread "main" java.lang.IllegalArgumentException: Cannot serialize Kotlin type com.example.Burger. Reflective serialization of Kotlin classes without using kotlin-reflect has undefined and unexpected behavior. Please use KotlinJsonAdapter from the moshi-kotlin artifact or use code gen from the moshi-kotlin- codegen artifact.
  36. Moshi ft. Kotlin val moshi = Moshi.Builder().build() val burgerAdapter: JsonAdapter<Burger>

    = moshi.adapter(Burger::class.java) class Burger(val name: String, val price: Int) @zsmb13 Exception in thread "main" java.lang.IllegalArgumentException: Cannot serialize Kotlin type com.example.Burger. Reflective serialization of Kotlin classes without using kotlin-reflect has undefined and unexpected behavior. Please use KotlinJsonAdapter from the moshi-kotlin artifact or use code gen from the moshi-kotlin- codegen artifact.
  37. val moshi = Moshi.Builder().build() val burgerAdapter: JsonAdapter<Burger> = moshi.adapter(Burger::class.java) class

    Burger(val name: String, val price: Int) @zsmb13 Moshi ft. Kotlin Exception in thread "main" java.lang.IllegalArgumentException: Cannot serialize Kotlin type com.example.Burger. Reflective serialization of Kotlin classes without using kotlin-reflect has undefined and unexpected behavior. Please use KotlinJsonAdapter from the moshi-kotlin artifact or use code gen from the moshi-kotlin- codegen artifact.
  38. val moshi = Moshi.Builder().build() val burgerAdapter: JsonAdapter<Burger> = moshi.adapter(Burger::class.java) class

    Burger(val name: String, val price: Int) def moshi_version = '1.9.2' implementation "com.squareup.moshi:moshi:$moshi_version" @zsmb13 Moshi: reflection
  39. val moshi = Moshi.Builder().build() Moshi: reflection val burgerAdapter: JsonAdapter<Burger> =

    moshi.adapter(Burger::class.java) class Burger(val name: String, val price: Int) def moshi_version = '1.9.2' implementation "com.squareup.moshi:moshi:$moshi_version" implementation "com.squareup.moshi:moshi-kotlin:$moshi_version" @zsmb13
  40. val moshi = Moshi.Builder() Moshi: reflection val burgerAdapter: JsonAdapter<Burger> =

    moshi.adapter(Burger::class.java) class Burger(val name: String, val price: Int) .build() @zsmb13
  41. Moshi: reflection .add(KotlinJsonAdapterFactory()) val burgerAdapter: JsonAdapter<Burger> = moshi.adapter(Burger::class.java) class Burger(val

    name: String, val price: Int) val json = burgerAdapter.toJson(Burger("Classic", 10)) val burger = burgerAdapter.fromJson(json) .build() val moshi = Moshi.Builder() @zsmb13
  42. Moshi: reflection def moshi_version = '1.9.2' implementation "com.squareup.moshi:moshi:$moshi_version" implementation "com.squareup.moshi:moshi-kotlin:$moshi_version"

    @zsmb13 Exception in thread "main" java.lang.IllegalArgumentException: Cannot serialize Kotlin type com.example.Burger. Reflective serialization of Kotlin classes without using kotlin-reflect has undefined and unexpected behavior. Please use KotlinJsonAdapter from the moshi-kotlin artifact or use code gen from the moshi-kotlin- codegen artifact.
  43. implementation moshi-kotlin:$moshi_version" "com.squareup.moshi: def moshi_version = '1.9.2' implementation "com.squareup.moshi:moshi:$moshi_version" @zsmb13

    Moshi: reflection Exception in thread "main" java.lang.IllegalArgumentException: Cannot serialize Kotlin type com.example.Burger. Reflective serialization of Kotlin classes without using kotlin-reflect has undefined and unexpected behavior. Please use KotlinJsonAdapter from the moshi-kotlin artifact or use code gen from the artifact. moshi-kotlin- codegen
  44. dependencies { } Moshi: codegen apply plugin: 'kotlin' apply plugin:

    'kotlin-kapt' def moshi_version = '1.9.2' implementation "com.squareup.moshi:moshi:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" @zsmb13
  45. @JsonClass(generateAdapter = true) class Burger(val name: String, val price: Int)

    val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) val json = burgerAdapter.toJson(Burger("Double", 18))) @zsmb13 Moshi: codegen
  46. dependencies { def moshi_version = '1.9.2' implementation "com.squareup.moshi:moshi:$moshi_version" implementation "com.squareup.moshi:moshi-kotlin:$moshi_version"

    kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" } apply plugin: 'kotlin' apply plugin: 'kotlin-kapt' val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build() @zsmb13 Moshi: mixed use
  47. Moshi: mixed use dependencies { def moshi_version = '1.9.2' implementation

    "com.squareup.moshi:moshi:$moshi_version" implementation "com.squareup.moshi:moshi-kotlin:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" } apply plugin: 'kotlin' apply plugin: 'kotlin-kapt' val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build() @zsmb13
  48. dependencies { def moshi_version = '1.9.2' implementation "com.squareup.moshi:moshi:$moshi_version" implementation "com.squareup.moshi:moshi-kotlin:$moshi_version"

    kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" } apply plugin: 'kotlin' apply plugin: 'kotlin-kapt' val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build() @zsmb13 Moshi: mixed use
  49. class CustomBurgerAdapter { TODO("Implement deserialization") @ToJson fun toJson(writer: JsonWriter, value:

    Burger?) { TODO("Implement serialization") } } @FromJson fun fromJson(reader: JsonReader): Burger? { } @zsmb13 Moshi: custom adapters
  50. Moshi: custom adapters @FromJson fun fromJson(reader: JsonReader): Burger? { var

    name: String? = null var price: Int? = null } @zsmb13
  51. Moshi: custom adapters @FromJson fun fromJson(reader: JsonReader): Burger? { var

    name: String? = null var price: Int? = null reader.beginObject() reader.endObject() } @zsmb13
  52. Moshi: custom adapters @FromJson fun fromJson(reader: JsonReader): Burger? { var

    name: String? = null var price: Int? = null reader.beginObject() while (reader.hasNext()) { } reader.endObject() } @zsmb13
  53. Moshi: custom adapters @FromJson fun fromJson(reader: JsonReader): Burger? { var

    name: String? = null var price: Int? = null reader.beginObject() while (reader.hasNext()) { when (reader.nextName()) { "name" -> name = reader.nextString() "price" -> price = reader.nextInt() else -> reader.skipValue() } } reader.endObject() } @zsmb13
  54. @FromJson fun fromJson(reader: JsonReader): Burger? { var name: String? =

    null var price: Int? = null reader.beginObject() while (reader.hasNext()) { when (reader.nextName()) { "name" -> name = reader.nextString() "price" -> price = reader.nextInt() else -> reader.skipValue() } } reader.endObject() return Burger(name!!, price!!) } @zsmb13 : custom adapters Moshi
  55. class BurgerJsonAdapter( moshi: Moshi ) : JsonAdapter<Burger>() { override fun

    toString(): String = buildString(28) { append("GeneratedJsonAdapter(").append("Burger").append(')') } private val options: JsonReader.Options = JsonReader.Options.of("name", "price") private val stringAdapter: JsonAdapter<String> = moshi.adapter(String::class.java, emptySet(), "name") private val intAdapter: JsonAdapter<Int> = moshi.adapter(Int::class.java, emptySet(), "price") override fun fromJson(reader: JsonReader): Burger { var name: String? = null var price: Int? = null reader.beginObject() while (reader.hasNext()) { when (reader.selectName(options)) { 0 -> name = stringAdapter.fromJson(reader) ?: throw Util.unexpectedNull("name", "name", reader) 1 -> price = intAdapter.fromJson(reader) ?: throw Util.unexpectedNull("price", "price", reader) -1 -> { reader.skipName() reader.skipValue() } } } reader.endObject() return Burger( name = name ?: throw Util.missingProperty("name", "name", reader), price = price ?: throw Util.missingProperty("price", "price", reader) ) } override fun toJson(writer: JsonWriter, value: Burger?) { if (value == null) { throw NullPointerException( "value was null! Wrap in .nullSafe() to write nullable values.") } writer.beginObject() writer.name("name") stringAdapter.toJson(writer, value.name) writer.name("price") intAdapter.toJson(writer, value.price) writer.endObject() } } @zsmb13 Moshi’s generated adapter
  56. class BurgerJsonAdapter( moshi: Moshi ) : JsonAdapter<Burger>() { override fun

    toString(): String = buildString(28) { append("GeneratedJsonAdapter(").append("Burger").append(')') } private val options: JsonReader.Options = JsonReader.Options.of("name", "price") private val stringAdapter: JsonAdapter<String> = moshi.adapter(String::class.java, emptySet(), "name") private val intAdapter: JsonAdapter<Int> = moshi.adapter(Int::class.java, emptySet(), "price") Moshi’s generated adapter @zsmb13
  57. private val intAdapter: JsonAdapter<Int> = moshi.adapter(Int::class.java, emptySet(), "price") override fun

    fromJson(reader: JsonReader): Burger { var name: String? = null var price: Int? = null reader.beginObject() while (reader.hasNext()) { when (reader.selectName(options)) { 0 -> name = stringAdapter.fromJson(reader) ?: throw Util.unexpectedNull("name", "name", reader) 1 -> price = intAdapter.fromJson(reader) ?: throw Util.unexpectedNull("price", "price", reader) -1 -> { reader.skipName() reader.skipValue() } } } reader.endObject() Moshi’s generated adapter @zsmb13
  58. private val intAdapter: JsonAdapter<Int> = moshi.adapter(Int::class.java, emptySet(), "price") override fun

    fromJson(reader: JsonReader): Burger { var name: String? = null var price: Int? = null reader.beginObject() while (reader.hasNext()) { when (reader.selectName(options)) { 0 -> name = stringAdapter.fromJson(reader) ?: throw Util.unexpectedNull("name", "name", reader) 1 -> price = intAdapter.fromJson(reader) ?: throw Util.unexpectedNull("price", "price", reader) -1 -> { reader.skipName() reader.skipValue() } } } reader.endObject() Moshi’s generated adapter @zsmb13
  59. private val intAdapter: JsonAdapter<Int> = moshi.adapter(Int::class.java, emptySet(), "price") override fun

    fromJson(reader: JsonReader): Burger { var name: String? = null var price: Int? = null reader.beginObject() while (reader.hasNext()) { when (reader.selectName(options)) { 0 -> name = stringAdapter.fromJson(reader) ?: throw Util.unexpectedNull("name", "name", reader) 1 -> price = intAdapter.fromJson(reader) ?: throw Util.unexpectedNull("price", "price", reader) -1 -> { reader.skipName() reader.skipValue() } } } reader.endObject() Moshi’s generated adapter @zsmb13
  60. private val intAdapter: JsonAdapter<Int> = moshi.adapter(Int::class.java, emptySet(), "price") override fun

    fromJson(reader: JsonReader): Burger { var name: String? = null var price: Int? = null reader.beginObject() while (reader.hasNext()) { when (reader.selectName(options)) { 0 -> name = stringAdapter.fromJson(reader) ?: throw Util.unexpectedNull("name", "name", reader) 1 -> price = intAdapter.fromJson(reader) ?: throw Util.unexpectedNull("price", "price", reader) -1 -> { reader.skipName() reader.skipValue() } } } reader.endObject() Moshi’s generated adapter @zsmb13
  61. private val intAdapter: JsonAdapter<Int> = moshi.adapter(Int::class.java, emptySet(), "price") override fun

    fromJson(reader: JsonReader): Burger { var name: String? = null var price: Int? = null reader.beginObject() while (reader.hasNext()) { when (reader.selectName(options)) { 0 -> name = stringAdapter.fromJson(reader) ?: throw Util.unexpectedNull("name", "name", reader) 1 -> price = intAdapter.fromJson(reader) ?: throw Util.unexpectedNull("price", "price", reader) -1 -> { reader.skipName() reader.skipValue() } } } reader.endObject() Moshi’s generated adapter @zsmb13
  62. override fun toString(): String = buildString(28) { append("GeneratedJsonAdapter(").append("Burger").append(')') } private

    val options: JsonReader.Options = JsonReader.Options.of("name", "price") private val stringAdapter: JsonAdapter<String> = moshi.adapter(String::class.java, emptySet(), "name") private val intAdapter: JsonAdapter<Int> = moshi.adapter(Int::class.java, emptySet(), "price") override fun fromJson(reader: JsonReader): Burger { var name: String? = null var price: Int? = null reader.beginObject() while (reader.hasNext()) { when (reader.selectName(options)) { 0 -> name = stringAdapter.fromJson(reader) ?: throw Util.unexpectedNull("name", "name", reader) Moshi’s generated adapter @zsmb13
  63. private val intAdapter: JsonAdapter<Int> = moshi.adapter(Int::class.java, emptySet(), "price") override fun

    fromJson(reader: JsonReader): Burger { var name: String? = null var price: Int? = null reader.beginObject() while (reader.hasNext()) { when (reader.selectName(options)) { 0 -> name = stringAdapter.fromJson(reader) ?: throw Util.unexpectedNull("name", "name", reader) 1 -> price = intAdapter.fromJson(reader) ?: throw Util.unexpectedNull("price", "price", reader) -1 -> { reader.skipName() reader.skipValue() } } } reader.endObject() Moshi’s generated adapter @zsmb13
  64. private val options: JsonReader.Options = JsonReader.Options.of("name", "price") private val stringAdapter:

    JsonAdapter<String> = moshi.adapter(String::class.java, emptySet(), "name") private val intAdapter: JsonAdapter<Int> = moshi.adapter(Int::class.java, emptySet(), "price") override fun fromJson(reader: JsonReader): Burger { var name: String? = null var price: Int? = null reader.beginObject() while (reader.hasNext()) { when (reader.selectName(options)) { 0 -> name = stringAdapter.fromJson(reader) ?: throw Util.unexpectedNull("name", "name", reader) 1 -> price = intAdapter.fromJson(reader) ?: throw Util.unexpectedNull("price", "price", reader) -1 -> { Moshi’s generated adapter @zsmb13
  65. throw Util.unexpectedNull("price", "price", reader) -1 -> { reader.skipName() reader.skipValue() }

    } } reader.endObject() return Burger( name = name ?: throw Util.missingProperty("name", "name", reader), price = price ?: throw Util.missingProperty("price", "price", reader) ) } } Moshi’s generated adapter @zsmb13
  66. throw Util.unexpectedNull("price", "price", reader) -1 -> { reader.skipName() reader.skipValue() }

    } } reader.endObject() return Burger( name = name ?: throw Util.missingProperty("name", "name", reader), price = price ?: throw Util.missingProperty("price", "price", reader) ) } } @zsmb13 Moshi’s generated adapter
  67. Moshi: built-in types val moshi = Moshi.Builder().build() val adapter: JsonAdapter<Date>

    = moshi.adapter(Date::class.java) Exception in thread "main" java.lang.IllegalArgumentException: Platform class java.util.Date requires explicit JsonAdapter to be registered @zsmb13
  68. Moshi: built-in types def moshi_version = '1.9.2' implementation "com.squareup.moshi:moshi:$moshi_version" kapt

    "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" val moshi = Moshi.Builder().build() val adapter: JsonAdapter<Date> = moshi.adapter(Date::class.java) @zsmb13
  69. Moshi: built-in types def moshi_version = '1.9.2' implementation "com.squareup.moshi:moshi:$moshi_version" kapt

    "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" implementation "com.squareup.moshi:moshi-adapters:$moshi_version" val moshi = Moshi.Builder().build() val adapter: JsonAdapter<Date> = moshi.adapter(Date::class.java) @zsmb13
  70. val adapter: JsonAdapter<Date> = moshi.adapter(Date::class.java) def moshi_version = '1.9.2' implementation

    "com.squareup.moshi:moshi:$moshi_version" implementation "com.squareup.moshi:moshi-adapters:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" .add(Date::class.java, Rfc3339DateJsonAdapter()) .build() val moshi = Moshi.Builder() @zsmb13 Moshi: built-in types
  71. val adapter: JsonAdapter<Date> = moshi.adapter(Date::class.java) def moshi_version = '1.9.2' implementation

    "com.squareup.moshi:moshi:$moshi_version" implementation "com.squareup.moshi:moshi-adapters:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" .add(Date::class.java, Rfc3339DateJsonAdapter()) 2020-01-30T19:03:50.250Z .build() val moshi = Moshi.Builder() @zsmb13 Moshi: built-in types
  72. @JsonClass(generateAdapter = true) data class Burger( @Json(name = "name") val

    name: String, @Json(name = "total_cost") val totalCost: Int, @Json(name = "sales_price") val salesPrice: Int ) @zsmb13 Moshi: field naming
  73. @JsonClass(generateAdapter = true) data class Burger( @Json(name = "name") val

    name: String, @Json(name = "total_cost") val totalCost: Int, @Json(name = "sales_price") val salesPrice: Int ) moshi.adapter(Burger::class.java) .toJson(Burger("Classic", 10, 13)) @zsmb13 Moshi: field naming
  74. @JsonClass(generateAdapter = true) data class Burger( @Json(name = "name") val

    name: String, @Json(name = "total_cost") val totalCost: Int, @Json(name = "sales_price") val salesPrice: Int ) moshi.adapter(Burger::class.java) .toJson(Burger("Classic", 10, 13)) {"name":"Classic","total_cost":10,"sales_price":13} @zsmb13 Moshi: field naming
  75. @JsonClass(generateAdapter = true) data class Burger( val name: String, val

    total_cost: Int, val sales_price: Int ) moshi.adapter(Burger::class.java) .toJson(Burger("Classic", 10, 13)) {"name":"Classic","total_cost":10,"sales_price":13} @zsmb13 Moshi: field naming
  76. @JsonClass(generateAdapter = true) data class Burger( val name: String, val

    price: Int ) Moshi: missing data val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) val burger = burgerAdapter.fromJson( "{\"name\": \"Cheeseburger\"}" ) @zsmb13
  77. @JsonClass(generateAdapter = true) data class Burger( val name: String, val

    price: Int ) Moshi: missing data val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) val burger = burgerAdapter.fromJson( "{\"name\": \"Cheeseburger\"}" ) Exception in thread "main" com.squareup.moshi.JsonDataException: Required value 'price' missing at $ @zsmb13
  78. @JsonClass(generateAdapter = true) data class Burger( val name: String, val

    description: String ) Moshi: missing data val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) val burger = burgerAdapter.fromJson( "{\"name\": \"Cheeseburger\"}" ) @zsmb13
  79. @JsonClass(generateAdapter = true) data class Burger( val name: String, val

    description: String ) Moshi: missing data val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) val burger = burgerAdapter.fromJson( "{\"name\": \"Cheeseburger\"}" ) Exception in thread "main" com.squareup.moshi.JsonDataException: Required value 'description' missing at $ @zsmb13
  80. @JsonClass(generateAdapter = true) data class Burger( val name: String, val

    description: String? ) Moshi: missing data val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) val burger = burgerAdapter.fromJson( "{\"name\": \"Cheeseburger\"}" ) @zsmb13
  81. @JsonClass(generateAdapter = true) data class Burger( val name: String, val

    description: String? ) Moshi: missing data val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) val burger = burgerAdapter.fromJson( "{\"name\": \"Cheeseburger\"}" ) Burger(name=Cheeseburger, description=null) @zsmb13
  82. Burger(name=Cheeseburger, description=null) @JsonClass(generateAdapter = true) data class Burger( val name:

    String, val description: String = "One of our best burgers!" ) Moshi: missing data val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) val burger = burgerAdapter.fromJson( "{\"name\": \"Cheeseburger\"}" ) @zsmb13
  83. Burger(name=Cheeseburger, description=null) @JsonClass(generateAdapter = true) data class Burger( val name:

    String, val description: String = "One of our best burgers!" ) Moshi: missing data val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) val burger = burgerAdapter.fromJson( "{\"name\": \"Cheeseburger\"}" ) Burger(name=Cheeseburger, description=One of our best burgers!) @zsmb13
  84. val moshi = Moshi.Builder() Moshi: customization options @JsonClass(generateAdapter = true)

    data class Burger(val name: String, val price: Int) .build() val burgerAdapter = moshi.adapter(Burger::class.java) val burger = burgerAdapter.fromJson( "{\"name\": \"Cheeseburger\", \"price\": 13}" ) @zsmb13
  85. Moshi: customization options @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) .add(...) val burgerAdapter = moshi.adapter(Burger::class.java) val burger = burgerAdapter.fromJson( "{\"name\": \"Cheeseburger\", \"price\": 13}" ) .build() val moshi = Moshi.Builder() @zsmb13
  86. val moshi = Moshi.Builder() Moshi: customization options @JsonClass(generateAdapter = true)

    data class Burger(val name: String, val price: Int) .build() val burgerAdapter = moshi.adapter(Burger::class.java) val burger = burgerAdapter.fromJson( "{\"name\": \"Cheeseburger\", \"price\": 13}" ) @zsmb13
  87. Moshi: customization options @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) JsonAdapter<Burger> @zsmb13
  88. Moshi: customization options @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) @zsmb13
  89. Moshi: customization options @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int?) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) burgerAdapter.toJson(Burger("Cancelled", null)) @zsmb13
  90. Moshi: customization options @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int?) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) burgerAdapter.toJson(Burger("Cancelled", null)) {"name":"Cancelled"} @zsmb13
  91. Moshi: customization options @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int?) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) .serializeNulls() burgerAdapter.toJson(Burger("Cancelled", null)) {"name":"Cancelled","price":null} @zsmb13
  92. Moshi: customization options @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) burgerAdapter.toJson(null) @zsmb13
  93. Moshi: customization options @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) burgerAdapter.toJson(null) null @zsmb13
  94. Moshi: customization options @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) .nullSafe() burgerAdapter.toJson(null) null @zsmb13
  95. Moshi: customization options @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) .nonNull() burgerAdapter.toJson(null) @zsmb13
  96. Moshi: customization options @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) .nonNull() burgerAdapter.toJson(null) Exception in thread "main" com.squareup.moshi.JsonDataException: Unexpected null at $ @zsmb13
  97. Moshi: customization options @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) @zsmb13
  98. Moshi: customization options @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) burgerAdapter.fromJson( "{\"name\":\"Cheeseburger\",\"price\": 13}" ) @zsmb13
  99. Moshi: customization options @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) burgerAdapter.fromJson( "{\"name\":\"Cheeseburger\",\"price\": 13}" ) Burger(name=Cheeseburger, price=13) @zsmb13
  100. Moshi: customization options @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) burgerAdapter.fromJson( "{\"name\":\"Cheeseburger\",\"price\": 13,\"rating\":4.2}" ) Burger(name=Cheeseburger, price=13) @zsmb13
  101. @JsonClass(generateAdapter = true) data class Burger(val name: String, val price:

    Int) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) .failOnUnknown() burgerAdapter.fromJson( "{\"name\":\"Cheeseburger\",\"price\": 13,\"rating\":4.2}" ) Moshi: customization options @zsmb13
  102. @JsonClass(generateAdapter = true) data class Burger(val name: String, val price:

    Int) val moshi = Moshi.Builder().build() val burgerAdapter = moshi.adapter(Burger::class.java) .failOnUnknown() burgerAdapter.fromJson( "{\"name\":\"Cheeseburger\",\"price\": 13,\"rating\":4.2}" ) Moshi: customization options Exception in thread "main" com.squareup.moshi.JsonDataException: Cannot skip unexpected NAME at $.rating @zsmb13
  103. @JsonClass(generateAdapter = true) data class Burger(val name: String, val price:

    Int) @JsonClass(generateAdapter = true) data class Restaurant(val menu: List<Burger>) @zsmb13 Moshi: malformed data
  104. @JsonClass(generateAdapter = true) data class Burger(val name: String, val price:

    Int) @JsonClass(generateAdapter = true) data class Restaurant(val menu: List<Burger>) val json = """{ "menu": [ { "name": "Classic", "price": 10 }, { "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "crocodile" } ] }""" @zsmb13 Moshi: malformed data
  105. @JsonClass(generateAdapter = true) data class Burger(val name: String, val price:

    Int) @JsonClass(generateAdapter = true) data class Restaurant(val menu: List<Burger>) val json = """{ "menu": [ { "name": "Classic", "price": 10 }, { "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "crocodile" } ] }""" val restaurant = adapter.fromJson(json, Restaurant::class.java) @zsmb13 Moshi: malformed data
  106. Moshi: malformed data @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) @JsonClass(generateAdapter = true) data class Restaurant(val menu: List<Burger>) val json = """{ "menu": [ { "name": "Classic", "price": 10 }, { "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "crocodile" } ] }""" val restaurant = adapter.fromJson(json, Restaurant::class.java) Exception in thread "main" com.squareup.moshi.JsonDataException: Expected an int but was crocodile at path $.menu[2].price @zsmb13
  107. val json = val restaurant = adapter.fromJson(json, Restaurant::class.java) """{ "menu":

    [ { "name": "Classic", "price": 10 }, { "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "crocodile" } ] }""" Moshi: malformed data @JsonClass(generateAdapter = true) data class Burger(val name: String, val price: Int) @JsonClass(generateAdapter = true) data class Restaurant(val menu: List<Burger>) Exception in thread "main" com.squareup.moshi.JsonDataException: Expected an int but was crocodile at path $.menu[2].price @zsmb13
  108. """{ "menu": [ { "name": "Classic", "price": 10 }, {

    "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "crocodile" } ] }""" Exception in thread "main" com.squareup.moshi.JsonDataException: Expected an int but was crocodile at path $.menu[2].price @zsmb13
  109. Exception in thread "main" com.squareup.moshi.JsonDataException: Expected an int but was

    crocodile at path $.menu[2].price """{ "menu": [ { "name": "Classic", "price": 10 }, { "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "crocodile" } ] }""" @zsmb13
  110. Exception in thread "main" com.squareup.moshi.JsonDataException: Expected an int but was

    crocodile at path $.menu[2].price """{ "menu": [ { "name": "Classic", "price": 10 }, { "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "crocodile" } ] }""" @zsmb13
  111. Exception in thread "main" com.squareup.moshi.JsonDataException: Expected an int but was

    crocodile at path $.menu[2].price """{ "menu": [ { "name": "Classic", "price": 10 }, { "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "crocodile" } ] }""" @zsmb13
  112. Moshi: malformed data @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) @JsonClass(generateAdapter = true) data class Restaurant(val menu: List<Burger>) val json = val restaurant = adapter.fromJson(json, Restaurant::class.java) """{ "menu": [ { "name": "Classic", "price": 10 }, { "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "crocodile" } ] }""" Exception in thread "main" com.squareup.moshi.JsonDataException: Expected an int but was crocodile at path $.menu[2].price @zsmb13
  113. Moshi: malformed data @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) @JsonClass(generateAdapter = true) data class Restaurant(val menu: List<Burger>) val json = val restaurant = adapter.fromJson(json, Restaurant::class.java) """{ "menu": [ { "name": "Classic", "price": 10 }, { "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "crocodile" } ] }""" Exception in thread "main" com.squareup.moshi.JsonDataException: Expected an int but was crocodile at path $.menu[2].price Exception in thread "main" com.google.gson.JsonSyntaxException: java.lang.NumberFormatException: For input string: "crocodile" @zsmb13
  114. Moshi: malformed data @JsonClass(generateAdapter = true) data class Burger(val name:

    String, val price: Int) @JsonClass(generateAdapter = true) data class Restaurant(val menu: List<Burger>) val json = val restaurant = adapter.fromJson(json, Restaurant::class.java) """{ "menu": [ { "name": "Classic", "price": 10 }, { "name": "Cheeseburger", "price": 13 }, { "name": "Double", "price": "" } ] }""" @zsmb13 Exception in thread "main" com.squareup.moshi.JsonDataException: Expected an int but was at path $.menu[2].price Exception in thread "main" com.google.gson.JsonSyntaxException: java.lang.NumberFormatException: For input string: ""
  115. Retrofit.Builder() .baseUrl("...") .addConverterFactory(MoshiConverterFactory.create()) .build() def retrofit_version = '2.7.1' implementation "com.squareup.retrofit2:retrofit:$retrofit_version"

    implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version" val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .add(...) .build() @zsmb13 Moshi: Retrofit integration
  116. Retrofit.Builder() .baseUrl("...") .addConverterFactory(MoshiConverterFactory.create( .build() def retrofit_version = '2.7.1' implementation "com.squareup.retrofit2:retrofit:$retrofit_version"

    implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version" val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .add(...) .build() @zsmb13 Moshi: Retrofit integration )) moshi
  117. More Moshi… •Other built-in adapters •Intermediate objects while parsing •Annotated

    types •Parsing arrays and other generic types •A lot more under-the-hood details! @zsmb13
  118. Resources •A Few Ok Libraries (Jake Wharton, Square) – Droidcon

    Montreal 2015 • https://www.youtube.com/watch?v=WvyScM_S88c •Upgrading to Moshi (Eric Cochran, Pinterest) - droidcon NYC 2017 • https://www.youtube.com/watch?v=xHFs7cccnXU •JSON Explained (Jesse Wilson, Square) – Chicago Roboto 2019 • https://www.youtube.com/watch?v=1PwdqkKDCSo @zsmb13
  119. Resources •Moshi, another JSON Processor (Jesse Wilson) • https://developer.squareup.com/blog/moshi-another-json-processor/ •Exploring

    Moshi’s codegen (Zac Sweers) • https://www.zacsweers.dev/exploring-moshis-kotlin-code-gen/ •Reflection Machines (Jesse Wilson) • https://publicobject.com/2016/03/24/reflection-machines/ •Advanced JSON parsing techniques using Moshi and Kotlin (Christophe Beyls) • https://medium.com/@BladeCoder/advanced-json-parsing-techniques- using-moshi-and-kotlin-daf56a7b963d @zsmb13
  120. zsmb.co/talks zsmb13 Márton Braun › Reflection or codegen › Kotlin

    friendly › Really fast › Great failure modes Say “hi” to Moshi