Null-safe network response in Kotlin

One of the Kotlin’s best features is null safety.
It gives us compile-time knowledge of where null can be and where not. While this works perfect in pure Kotlin code, null-safety cannot be guaranteed when Java interoperation begins. There are many use-cases of Java-interop and just as many ways to increase the level of safety when dealing with it. One of the most common use cases is network communication. Let’s see how we can make it safe.

I will use JSON and GSON as an example, but the idea remains the same regardless of the format and deserialization method.

The idea

Suppose we have the following response:

{
  "id": "some.email@example.com",   // required
  "name": "John",                   // optional
  "surname": "Smith",               // optional
  "age": 42                         // optional
}

Note that some fields are required, and some are optional.
In Java it would be mapped to a class similar to the following

public class User {
    
    private final String id;
    private final String name;
    private final String surname;
    private final int age;

    public User(String id, String name, String surname, int age) {
        this.id = id;
        this.name = name;
        this.surname = surname;
        this.age = age;
    }

    public String getId() { return id; }
    public String getName() { return name; }
    public String getSurname() { return surname; }
    public int getAge() { return age; }
}

It has a lot of boilerplate, but that’s a different topic.
The goal of the current article is to show how this kind of classes can be rewritten in Kotlin in much safer way.

GSON does not have the concept of optional fields. And even if it had, it’s hard to specify that a field in a Java class is mandatory or optional. The best way to do this in Java is to annotate mandatory fields with @NotNull, and then raise the level of not-null IDE check from “warning” to “error”. Unfortunately not many developers do this.

In Kotlin, thanks to the null-safety, you can declare optional fields of your class as nullable, and this will be a compile-time guarantee!

This is how the basic version of the same class will look in Kotlin:

data class User(
        val id: String,
        val name: String?,
        val surname: String?,
        val age: Int?
)

Note the round parentheses! This is not the body of the class, this is the primary constructor.
The properties are at the same time constructor arguments.

What is compile-time guarantee

Compile-time guarantee means that compilation will fail if you try to use this class in an inappropriate way. In case of IntellijIDEA (and Android Studio) the inappropriate usage of the class will be highlighted in red immediately:

Protecting from Java

This class would be sufficient, if the underlying deserialization is written in Kotlin and knows about Kotlin’s null-safety. But as most deserializers come from the Java world, additional issues arise.

Reflection

The first trick here is reflection: as GSON uses it to assign values to the fields, it can bypass Kotlin’s checks and set null into a field declared as non-nullable.
The following code

val invalidResponse = """{ "name": "John" }"""
val user: User = Gson().fromJson(invalidResponse, User::class.java)
val idLength = user.id.length()

would produce an NPE at the last line, as Gson would set null into the non-nullable field
via reflection.

In order to make sure that there are no nulls in non-nullable fields, we have to validate them after parsing. It can be done in different ways, the most straightforward of them is just a validate() method like this

data class User(
        val id: String,
        val name: String?,
        val surname: String?,
        val age: Int?
) {
    fun validate() {
        if (id == null) throw JsonParseException("'id' is null!")
    }
}

While this implementation is pretty naive and can be improved in different ways, it shows the idea.
By the way, you can also check here that values are in valid range within type, like positive integers or non-empty strings.

Primitive fields

The second trick is related to mandatory fields of primitive types.
Suppose that the “age” property becomes mandatory for some reason.
Than the User class would change:

public data class User(
        val id: String,
        val name: String?,
        val surname: String?,
        val age: Int           // non-nullable
)

Int type gets translated to Java int, while Int? is translated to Integer. So, if a mandatory primitive field is missing in JSON (which is an error), it will be set to its default value in Java, (e.g 0 for int), and not null. After that we will not be able to detect this error with if-not-null check:

if (age == null) throw JsonParseException("'age' is null!")

would not throw, because age == 0.
The workaround here is to declare an optional private field into which the value will be parsed, and a public non-nullable property that will just return the private property:

public data class User(
        val id: String,
        val name: String?,
        val surname: String?,
        private val _age: Int?   // Declared as private and nullable
) {
    
    val age: Int                 // non-nullable
        get() = _age!!
    
    fun validate() {
        if (id == null) throw JsonParseException("'id' is null!")
        if (_age == null) throw JsonParseException("'age' is null!")
    }
}

With this approach we can distinguish between 0 (which is valid) and null (which is not valid for a mandatory field).

Conclusion

Using this approach we can create classes that will represent server responses and give us compile-time knowledge of mandatory and optional fields.

This approach has other use cases, like working with database or any Java API that we cannot trust for some reason.
It requires us to write some additional code, so we should always analyze if it is worth implementing.

Source code can be found on the GitHub.

3 thoughts on “Null-safe network response in Kotlin”

    1. Anton Rutkevich Post Author

      Jacek, that’s actually not a language issue, it’s Java interoperability issue.

      But a JSON parsing library written in Kotlin could resolve it, you’re right.

Leave a Reply