12

I've recently started using Moshi for my Android app and I'm curious to know more about what the annotation @JsonClass(generateAdapter = true) really does.

Example data class:

data class Person(
    val name: String
)

I'm able to serialise/de-serialise this class as follows:

val moshi: Moshi = Moshi.Builder().build()
moshi.adapter(Person::class.java).toJson(Person())

I'm not using the @JsonClass annotation here and hence codegen won't kick in.

My question is, why and when do I need to use @JsonClass(generateAdapter = true)

Jignesh Shah
  • 389
  • 2
  • 13

2 Answers2

19

This is sort of three questions

  1. Why is code gen useful

Code gen is useful as a compile-time alternative to the reflective moshi-kotlin. Both of them are useful because they natively understand Kotlin code and its language features. Without them, Moshi would not be able to understand Kotlin nullability, default values, and much more. There are cases where Moshi works by coincidence with just standard Java reflection, your example above is one of them. This is super error prone though, and in Moshi 1.9 these will be rejected and require either a generated adapter or kotlin-reflect.

  1. How does it work

Code gen is an annotation processor that looks for classes annotated with @JsonClass(generateAdapter = true). It generates an optimized streaming adapter for each annotated class. These adapters are Kotlin themselves and thus capable of leveraging Kotlin language features that support the target class. At runtime, Moshi reflectively looks up the generated adapter with a very simple known name suffix, which allows these adapters to Just Work without manual adapter registration.

You can find more info on both 1 and 2 in my blog post about it when it was released: https://www.zacsweers.dev/exploring-moshis-kotlin-code-gen/

  1. When should it be used

You should use moshi-kotlin or code gen any time you are trying to serialize a Kotlin class with Moshi without your own custom adapter. Reflection will have no build time overhead but be far slower at runtime while also incurring a large binary size cost due to kotlin-reflect and cannot be safely obfuscated. Code gen incurs a build time cost but is extremely fast at runtime with minimal binary size cost and mostly obfuscation-safe. It's up to you which of these is better suited to your use case! You can also use a combination, such as reflection in debug builds and code gen just for release builds.

Zac Sweers
  • 2,875
  • 1
  • 15
  • 20
  • Thank you! I have a question wrt @user2340612's answer: If I understand this correctly, using ```@JsonClass(generateAdapter = true)``` basically avoids the "ClassJsonAdapter" and hence reflection? – Jignesh Shah Oct 25 '19 at 07:39
  • Right. It indicates to Moshi that there is a generated adapter that should be used. It’s also used by the Moshi code gen annotation processor to indicate which classes should have an adapter generated for them. – Zac Sweers Nov 03 '19 at 21:20
11

Earlier versions of Moshi didn't support "codegen", so they completely relied on reflection (i.e., the ability to introspect classes at runtime). However, that could be a problem for applications that require very high performance so they've added a "codegen" capability that takes advantage of annotation processing. Basically that allows generating code at compilation time, so they can perform serialisation/deserialisation without using reflection.

In order to enable the code generation functionality you also need to enable Kapt, which is the Kotlin annotation processor, otherwise no annotation processing will happen.

Here you'll find how to enable and configure Kapt, and here you'll find how to set up Moshi codegen dependencies.


EDIT

In the snippet of code you added to your question, Moshi uses ClassJsonAdapter to serialise your object. That adapter makes use of reflection to find all the fields and create the JSON string (you can see that class imports stuff from java.lang.reflect).

On the other hand, Moshi code gen generates a JsonAdapter for your class for you. For instance, letting Moshi create an adapter for your Person class will generate the following:

// Code generated by moshi-kotlin-codegen. Do not edit.
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import java.lang.NullPointerException
import kotlin.String

class PersonJsonAdapter(moshi: Moshi) : JsonAdapter<Person>() {
    private val options: JsonReader.Options = JsonReader.Options.of("name")

    private val stringAdapter: JsonAdapter<String> =
            moshi.adapter<String>(String::class.java, kotlin.collections.emptySet(), "name")

    override fun toString(): String = "GeneratedJsonAdapter(Person)"

    override fun fromJson(reader: JsonReader): Person {
        var name: String? = null
        reader.beginObject()
        while (reader.hasNext()) {
            when (reader.selectName(options)) {
                0 -> name = stringAdapter.fromJson(reader) ?: throw JsonDataException("Non-null value 'name' was null at ${reader.path}")
                -1 -> {
                    // Unknown name, skip it.
                    reader.skipName()
                    reader.skipValue()
                }
            }
        }
        reader.endObject()
        var result = Person(
                name = name ?: throw JsonDataException("Required property 'name' missing at ${reader.path}"))
        return result
    }

    override fun toJson(writer: JsonWriter, value: Person?) {
        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.endObject()
    }
}

As a result, when you ask for an adapter for Person, Moshi will use the generated PersonJsonAdapter instead of the ClassJsonAdapter.

Bonus

Moshi uses reflection to instantiate the generated adapter (which then gets cached, so that it gets reused next time you ask for an adapter for the same class), so that you don't need to add any extra code at all if you want to use the codegen functionality.

user2340612
  • 8,484
  • 4
  • 35
  • 61
  • Json serialisation/de-serialisation works without reflection and code-gen. My question is what's the advantage or real use of code gen? – Jignesh Shah Oct 23 '19 at 04:16
  • 1
    @JigneshShah please check my updated answer. I hope it's more clear now :) – user2340612 Oct 23 '19 at 08:53
  • Thank you for the very detailed response! If I understand this correctly, using ```@JsonClass(generateAdapter = true)``` basically avoids the "ClassJsonAdapter" and hence reflection? – Jignesh Shah Oct 25 '19 at 07:36
  • 1
    @JigneshShah basically yes, but only for classes annotated with `@JsonClass(generateAdapter=true)`. In other words, if you have 3 classes and only 1 has that annotation, then Moshi will generate an adapter only for the latter, while the other 2 classes will be serialized/deserialized using reflection – user2340612 Oct 25 '19 at 07:59
  • If generate adapter is only at the top level class, are adapters generated for nested classes or other referenced class objects? – Jignesh Shah Oct 26 '19 at 12:27
  • 1
    @JigneshShah no, the codegen will generate adapters only for classes that are annotated, so you need to manually annotate inner and/or otherwise referenced classes – user2340612 Oct 26 '19 at 17:54