Handling null values in Kotlin

The semantics of handling nulls.
Akshay Dewan_avatar
Akshay Dewan
Jul 07, 2021 | 8 min read

Introduction

Coming from Java development, one of the most prominent features of Kotlin is null safety. Although this feature is a huge benefit, it is not a silver bullet. Codebases tend to get littered with the !! operator and may end up causing NullPointerException.

The primary focus of this blog is the semantics of nulls, and how to manage code to avoid unnecessary null checks or !! operators everywhere. Since the internet is already swamped with a lot of good tutorials on syntax of using null handling operators, it will not be a part of our discussion.

Let’s start with looking at what Kotlin does for you - it provides type safety i.e. when you declare a variable as a String?, it can hold a null or a String. By default, the Kotlin compiler doesn’t allow a null value at compile-time.

Let’s dive deeper to find out more about the null value.

Why are the values null?

There could be many reasons for a null value. Let’s explore the following scenarios for better understanding:

  1. Nullable values in a business context
  2. Nullable return type from a function contract
  3. Nullable values from outside data (and human behavior)

Nullable values in a business context

Let's say you're building an application for an e-commerce retail store. While signing up, your users are prompted to enter their email address and full name. Optionally, they can also provide their phone number.

The representation of the User class may look like this:

data class User(
    val email: String,
    val fullName: String,
    val phoneNumber: String?
)

Now let's say that you are given a task to send out an SMS notification whenever a user makes a purchase. Since the phone number is nullable, your users have not provided their phone number. So, what do you do? Well, the first step would be to trigger a conversation with your BA or Product Owner. Following the conversation, you may be asked to either skip the SMS or send an email instead but never use the !! operator.

While this may be a very simple example, the domain tends to get quite complex in real projects and such instances can be overlooked. In other cases, null values can be avoided by better domain modeling.

The representation of the Order class may look like this:

data class Order(
    val item: String,
    val quantity: Int,
    val paymentMode: PaymentMode,
    val cardDetails: CardDetails?
)

enum class PaymentMode {
    CASH,
    CARD
}

data class CardDetails(
    val provider: String,
    val bank: String
)

In this example cardDetails will be null if the PaymentMode is CASH. But we can refactor the model as follows to avoid the nullable type:

data class Order(
    val item: String,
    val quantity: Int,
    val paymentMode: PaymentMode
)

sealed class PaymentMode

object Cash : PaymentMode()

data class Card(
    val provider: String,
    val bank: String
) : PaymentMode()

This way, the Order class doesn't need to have a nullable cardDetails type. The when keyword can be used when accessing order.paymentMode to check what is the type of the payment mode.

Nullable return type as a function contract

For this scenario, let’s take the example of Kotlin's own standard library.

val collection = listOf("apples", "bananas", "mangoes")

val result = collection.find { it == "oranges" }

Here, the result is null. The function contract has been defined to handle situations where we do not find what we are looking for.

How you deal with this depends on the context. If "oranges" was a user-entered value, then you may want to show an error saying that the requested fruit is not found.

You can use similar contracts for the functions. For example, if you do not find the record you are looking for in the database, return a nullable type from your function. How the consumer handles the function is their lookout.

It is usually a bad idea to use the !! operator when you handle the result of such functions. To handle it correctly, you may have to ask a business question and think about error handling and edge cases.

Null values from outside data (and human behavior)

Outside data is data that comes into the system from an integration point. An integration point is any external system that we communicate with.

For example:

  • APIs (including APIs that communicate with a frontend)
  • Databases
  • Uploaded Files (e.g. an application monitoring an S3 bucket for CSV files)

Going back to the online e-commerce store, let's say that your wholesalers post their catalog and prices as a CSV file. You use this CSV file to update your database. Later, you use this data for re-filling inventory, calculating profits, etc.

The (simplified) CSV file may look something like this:

item_id,name,price,expiry_date,item_type
987,Toothbrush,20,,NON_PERISHABLE,
648,Fountain Pen,50,,NON_PERISHABLE
375,Fresh Vegetables,30,2021-05-04,PERISHABLE
(..and so on...)

Note that you have both perishable (have an expiry date) and non-perishable (do not expire) items.

When you receive the file, you ingest it and update the catalog in your database. In Kotlin, you may create a corresponding data model for this CSV:

data class WholesalerCatalogLine(
    val itemId: String,
    val name: String,
    val price: BigDecimal,
    val itemType: String,
    val expiryDate: Date?,
)

Since we spoke about nulls and data models previously in this post, you might have noticed that our class is a reflection of the CSV, but not how the real domain model should be. As a programmer working on this task, I would be focused on data ingestion and not domain modeling. And as far as ingestion is concerned, what we have done may be sufficient. The problem starts when a couple of weeks down the road, another programmer takes this forward and starts using the same class for building another feature, as an inventory report. We're lazy humans, and we always tend to take the path of least resistance.

A better domain model for the wholesaler catalog might look something like this:

sealed class WholesalerCatalogItem {
    abstract val itemId: String
    abstract val name: String
    abstract val price: BigDecimal
}

data class NonPerishableItem(
    override val itemId: String,
    override val name: String,
    override val price: BigDecimal
): WholesalerCatalogItem()

data class PerishableItem(
    override val itemId: String,
    override val name: String,
    override val price: BigDecimal,
    val expiryDate: Date
): WholesalerCatalogItem()

I've taken the example of a CSV file, but the same problem can occur when we're building REST APIs and using a JSON to object mapping library. Let's take the same scenario, but instead of posting a CSV, the wholesaler is sending an API request:

[
    {
        "itemId": 987,
        "name": "Toothbrush",
        "price": 20,
        "item_type": "NON_PERISHABLE"
    },
    {
        "itemId": 648,
        "name": "Fountain Pen",
        "price": 50,
        "item_type": "NON_PERISHABLE"
    },
    {
        "itemId": 375,
        "name": "Fresh Vegetables",
        "price": 30,
        "expiry_date": "2021-05-04"
        "item_type": "PERISHABLE"
    }
]

Web Frameworks (like Spring and Micronaut) provide some means of directly mapping JSON to objects using libraries like Jackson or GSON, or any library that we want to plug in. The problem is, we tend to directly map the JSON structure to a class, and that class becomes our domain model.

In simple cases, this works just fine. But as the complexity of the model increases, mapping JSON objects to a class hierarchy becomes difficult. For example, to support polymorphic classes, some libraries may ask you to write custom serializers and deserializers. Jackson has annotations like @JsonSubTypes which can be used. But the solution is not trivial. And again, as humans, we tend to do what is easy - our domain model starts looking like the JSON representation from the API.

If you are using a database like MongoDB, you might see the same problem happen while reading from the database. Even the SQL databases have the same problem. You may use ORMs, but eventually, you may find that our classes actually look more like database tables than actual domain models.

So what can you do? Unfortunately, there is no straightforward solution, but there are things that you can keep in mind:

You need to be extra careful at your system boundaries. Patterns and practices like Layering, Domain-driven design, Hexagonal or Onion Architecture, Anti-corruption layers, etc. are very useful. The central idea behind these many architectures is to shield the "core" of the system from outside complexities. It is better to do validations and null handling at your system boundaries. It is not necessary to be very strict about these patterns - you can be pragmatic and pick and choose what suits your application. But it's good to be aware of them and what problems they solve.

Always think of the domain model when you implement business logic. Don't let your model be influenced by the schema of "outside data" from APIs, DB tables, etc.

Object mapping libraries can be a double-edged sword. For cases where they are difficult to use, you can have a separate Input or Request model and a separate Domain model.

In conclusion

Bugs are an inevitable part of software development (at least until AI takes over). While it may not be possible to entirely avoid the use of the !! operator, its presence might point to a code smell. I hope this blog post has been of some help to identify and potentially improve the quality of your code.