Authors Top

If you have a few years of experience with the Kotlin language and server-side development, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

1. Introduction

Moshi library is built on top of Okio and inherits the principles of Gson, another JSON parser. Unlike Gson, though, it’s much faster, and unlike Jackson, it has a much smaller footprint, which is important for embedded applications.

We’ve already discussed the application of Moshi in general. However, since it’s the library written mostly in Kotlin for Kotlin developers, let’s discuss the aspects of it specific to our language.

2. Basic Moshi Setup

To use Moshi with Kotlin, we need to define the following dependencies:

<dependency>
    <groupId>com.squareup.moshi</groupId>
    <artifactId>moshi</artifactId>
    <version>1.14.0</version>
</dependency>
<dependency>
    <groupId>com.squareup.moshi</groupId>
    <artifactId>moshi-adapters</artifactId>
    <version>1.14.0</version>
</dependency>
<dependency>
    <groupId>com.squareup.moshi</groupId>
    <artifactId>moshi-kotlin</artifactId>
    <version>1.14.0</version>
    <!-- Omit if using codegen exclusively -->
</dependency>

If we also plan to use Moshi codegen for our type adapters, we need to setup kapt execution in kotlin-maven-plugin before the compile step:

<!-- in kotlin-maven-plugin, executions section -->
<execution>
    <id>kapt</id>
    <goals>
        <goal>kapt</goal>
    </goals>
    <configuration>
        <sourceDirs>
            <sourceDir>src/main/kotlin</sourceDir>
        </sourceDirs>
        <annotationProcessorPaths>
            <annotationProcessorPath>
                <groupId>com.squareup.moshi</groupId>
                <artifactId>moshi-kotlin-codegen</artifactId>
                <version>1.14.0</version>
            </annotationProcessorPath>
        </annotationProcessorPaths>
    </configuration>
</execution>

2.1. Basic Serialization and Deserialization

Let’s imagine we have a simple task of accepting JSON input, transforming it somehow, and replying with another JSON message:

data class Department(
    val name: String,
    val code: UUID,
    val employees: List<Employee>
)

data class Employee(
    val firstName: String,
    val lastName: String,
    val title: String,
    val age: Int,
    val salary: BigDecimal
)

data class SalaryRecord(
    val employeeFirstName: String,
    val employeeLastName: String,
    val departmentCode: UUID,
    val departmentName: String,
    val sum: BigDecimal,
    val taxPercentage: BigDecimal
)

Moshi library works differently than Jackson and is similar to Gson. It uses reflection to serialize and deserialize data classes in Kotlin and POJOs in Java, but it requires explicit adapters to operate with platform types like UUID or BigDecimal.

It generally has fewer built-in adapters, both to prevent its users from locking themselves to a particular version of the JDK and to keep its footprint low:

class UuidAdapter : JsonAdapter<UUID>() {
    @FromJson
    override fun fromJson(reader: JsonReader): UUID? = UUID.fromString(reader.readJsonValue().toString())

    @ToJson
    override fun toJson(writer: JsonWriter, value: UUID?) {
        writer.jsonValue(value.toString())
    }
}

By default, Moshi provides adapters for primitive types, their boxed counterparts, all standard collections (Arrays, Lists, Maps, etc.), and Strings. The custom adapters we have to add manually to the Moshi instance:

val moshi = Moshi.Builder()
  .add(UuidAdapter())
  .add(BigDecimalAdapter()) // And all other adapters
  .addLast(KotlinJsonAdapterFactory())
  .build()

After that, we are ready to go:

val adapter = moshi.adapter<Department>()
val department = adapter.fromJson(resource("sales_department.json")!!.source().buffer())

val salaryRecordJsonAdapter = moshi.adapter<SalaryRecord>()
val serialized: String = salaryRecordJsonAdapter.toJson(record)

The Kotlin-specific syntax requires an opt-in annotation @ExperimentalStdlibApi on top of our class or method at the time this article is written.

2.2. On-the-fly and Pre-generated Adapters

The standard way of Moshi is to use reflection with user types to generate adapters. With Kotlin, it means an extra dependency and extra features: Moshi understands Kotlin’s non-nullability concept and correctly works with default values. The extra-dependency takes around 2.5Mb. Additionally, reflection-generated adapters can serialize and deserialize private and protected fields.

However, there’s a way to save time and disk space by using a moshi-kotlin-codegen annotation processor. It will generate adapters for our type at compile-time. These adapters will likely take less space than the reflection library and work faster. On the downside, they will only be able to work with internal and public fields.

To use codegen, we need to set up KAPT, as shown above. If we want to use it exclusively, we should delete moshi-kotlin from runtime dependencies. If we want to have both approaches at the same time, we should mark types for which we want to pre-generate adapters with a special annotation:

@JsonClass(generateAdapter = true)
data class Department( /* property declarations*/ )

After compilation, we will find DepartmentJsonAdapter.class file in the target directory.

2.3. Parsing Generic Types

Very often, the type we need to parse or produce is a list of objects. With Moshi in Java, it’s a bit complicated, as we need to create a new Type first. In Kotlin, thanks to reified generics, it’s simple:

val employeeListAdapter = moshi.adapter<List<Employee>>()

And then, the serialization and deserialization work in the same way:

val list = employeeListAdapter.fromJson(inputStream.source().buffer())

2.4. Working with Okio

For accessing IO operations, Moshi uses the Okio library, which is, in turn, a part of the OkHttp client. There is no way to parse an InputStream or File containing JSON into objects directly. Instead, we have to create a BufferedSource or JsonReader from them first:

val bufferedSource = inputStream.source().buffer()
val reader = JsonReader.of(bufferedSource)

Using the Okio library allows us to deal with big JSON values frugally, as we’ll see below.

3. Modifying Moshi Behaviour With Annotations

The Square team built Moshi for speed and optimal RAM utilization. Therefore, it doesn’t have too many customizable options, like property-naming strategies or WHAT ELSE.

However, there are some things that we can do via annotations. We already looked at @JsonClass usage, which invokes adapter generation. Let’s see what else we can do. This functionality is not specific to Kotlin, but it’s an essential part of Moshi library functionality.

3.1. Using Different Names in Objects and JSON

The most frequent case when the names at runtime and in JSON are different is when the code uses a different name style than the JSON. For instance, JSON is in snake_case, and the code is in camelCase. In Moshi, this can be solved by annotating all fields with their JSON names:

data class SnakeProject(
  @Json(name = "project_name")
  val snakeProjectName: String,
  @Json(name = "responsible_person_name")
  val snakeResponsiblePersonName: String,
  @Json(name = "project_budget")
  val snakeProjectBudget: BigDecimal
)

When we serialize this class, Moshi will use the names in the annotations:

val snakeProjectAdapter = moshi.adapter<SnakeProject>()
val snakeProject = SnakeProject(
  snakeProjectName = "Mayhem",
  snakeResponsiblePersonName = "Tailor Burden",
  snakeProjectBudget = BigDecimal("100000000"),
  snakeProjectSecret = "You're not a snowflake"
)
val stringProject = snakeProjectAdapter.toJson(snakeProject)

// stringProject equals {"project_name":"Mayhem","responsible_person_name":"Tailor Burden","project_budget":"100000000"}

3.2. Ignoring Fields

Similarly, not every runtime field is worth persisting or sending to the client. We can skip fields with @Json(ignore = true):

data class SnakeProject(
  // ... existing fields 
  @Json(ignore = true)
  val snakeProjectSecret: String = "No secret"
)

val stringProject = snakeProjectAdapter.toJson(snakeProject)

// stringProject is unchanged

One thing to note, though, is that the Moshi codegen will require us to set a default value for the ignored field so that the object can be instantiated properly during the parsing.

3.3. Choosing an Adapter by a Custom Annotation

Sometimes values represented by the same Kotlin type must be represented differently in JSON. A classic example of this is the representation of color in an RGB scheme. We are used to the color being a hexadecimal number, but for the JVM, it’s just another Int.

Fortunately, Moshi can assign an adapter based not only on the type of the value or field but also on an annotation. First, we need to create an annotation class:

@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class Hexadecimal

Then we need to create and register in the Moshi instance an adapter for serializing hexadecimal numbers and parsing them:

class HexadecimalAdapter {
    @ToJson
    fun toJson(@Hexadecimal color: Int): String = "#%06x".format(color)

    @FromJson
    @Hexadecimal
    fun fromJson(color: String): Int = color.substring(1).toInt(16)
}

The annotations on the parser argument and serializer function mark the adapter for the Moshi engine, which is then able to match the adapter to the field by annotation:

val moshi: Moshi = Moshi.Builder()
  // other adapters
  .add(HexadecimalAdapter())
  // the rest of the configuration

val paletteAdapter = moshi.adapter<Palette>()
val palette = Palette(
  83 * 256 * 256 + 43 * 256 + 18,
  160 * 256 + 18,
  25
)
val result = paletteAdapter.toJson(palette)

// result is {"mainColor":"#532b12","backgroundColor":"#00a012","frameFrequency":25}

4. Using Moshi to Parse a Stream of JSON Objects

Okio usage comes in very handy when the application resources are scarce. As we noticed earlier, often, we have to parse not a single JSON object but a collection of uniform objects – be it order items or customer records. If we cannot afford to read the whole JSON document into the memory, it’s time to try streaming. Luckily, Moshi adapts very well to this.

Moshi envelops a Source into a JsonReader – an entity that reads a JSON as a stream of tokens. Theoretically, we can create our own streaming parser for any type using JsonReader. For now, we only need a small helper method to read collection items one by one:

inline fun JsonReader.readArray(body: JsonReader.() -> Unit) {
    beginArray()
    while (hasNext()) {
        body()
    }
    endArray()
}

Then it’s easy to populate a Flow of items which then any coroutine can consume:

suspend inline fun <reified T> readToFlow(input: InputStream, adapter: JsonAdapter<T>): Flow<T> = flow {
    JsonReader.of(input.source().buffer())
      .readArray {
          emit(adapter.fromJson(this)!!)
      }
}

This reader will not read the document eagerly, reducing the memory necessary for the application. If the task was to reduce the document, we can do that without having all elements in memory at the same time:

val totalSalary = runBlocking {
    readToFlow(it, employeeAdapter)
      .fold(BigDecimal.ZERO) { acc, value -> acc + value.salary }
}

5. Conclusion

Moshi is a minimalistic but powerful JSON library. It requires some effort to get started – we must create all these adapters for the platform types we use in our code. But once underway, Moshi is faster than its predecessor Gson and won’t increase the size of our APK much.

In this tutorial, we looked at how to set up Moshi with Maven for a Kotlin project. Setting it up with Gradle is much easier. We talked about how we could customize Moshi’s behavior in order to suit our particular case. With annotations, we can alter names in the resulting JSON documents or make the input document names map to the naming strategy of our use.

Alternatively, we can discard some of the fields altogether or use different adapters for the same data type, depending on the protocol we are implementing. Finally, we discussed parsing large JSON collections using streaming techniques and saving on RAM.

All the examples, as always, can be found over on GitHub.

Authors Bottom

If you have a few years of experience with the Kotlin language and server-side development, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

Comments are closed on this article!