Understanding Null Safety in Kotlin
One of Kotlin’s best features is null safety.
It gives us compile-time knowledge of where null
can be and where not. While this works perfectly 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 class can be rewritten in Kotlin in a 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.
Suggested Read: 20 Kotlin interview Questions to Prepare
What is a compile-time guarantee
A compile-time guarantee means that the compilation will fail if you try to use this class in an inappropriate way. In the 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 invalid range within the 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.
Then 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 the 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 databases 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.
Thanks for reading
You can also read how to crack an interview Guide, Some people face a lot of difficulty in giving interviews and do not prepare and do not know how to face the interview, so according to your timing, make your interview perfect.
Top comments (0)