When null Becomes the Word "null"

For close to two years now I have been working on one Android app that has been in production a long time — Java at its core, MVP architecture, thousands of people opening it every day.

One task runs quietly in the background the whole time: migrating the code to Kotlin. Not a big project with a deadline, but instalments — one module every few weeks, fitted around new features. It is still half done, and still going.

This is about one kind of bug I kept running into along the way. I think it is the most characteristic Java-to-Kotlin migration bug there is, because it comes out of the very feature meant to protect you.

What the bug looks like

Picture a form that sends profile data to a server. After the migration to Kotlin, the code reads roughly like this:

params["city_id"] = user.selectedCity?.id.toString()
params["region_id"] = user.selectedRegion?.id.toString()

It looks safe. There is a ?., so if no city has been picked yet it will not crash.

And it does not crash. What happens is quieter: the server receives the string "null" — four characters, n-u-l-l — and then fails to parse it as a number.

Why it happens

Look at where ?. stops working:

user.selectedCity?.id.toString()
//               ^^^          ^^^
//               safe         NOT safe

?. only guards the .id call. If selectedCity is null, the expression selectedCity?.id evaluates to null — and the .toString() that follows is called on that null, not skipped.

In Kotlin, null.toString() is not an error. It calls the Any?.toString() extension, which dutifully returns the string "null".

So instead of a crash you can see, you get dirty data that travels all the way to the server.

The fix is one operator, plus one call:

params["city_id"] = user.selectedCity?.id?.toString().orEmpty()
//                                       ^
//                        a second ?., and .orEmpty() at the end

Now null becomes an empty string rather than the word “null”.

Why it is interesting

This bug cannot happen in Java.

In Java, user.getSelectedCity().getId() on a null object throws a NullPointerException immediately. Noisy, but honest — you know something is wrong, and you know which line.

Kotlin offers ?. so you do not have to write nested null checks. But use the operator only halfway and what you get is not safety — just a quieter failure. A crash turns into corrupted data, and corrupted data is far more expensive to find.

The awkward part: this bug never shows up on the happy path. As long as users fill in every field, everything is fine. It only appears when an optional field is skipped — which is exactly the case that gets tested least.

Two things I took from this migration

Null-safety is a property of system boundaries, not of a language. Kotlin guards what happens inside its own code. What leaves over the network is just strings — and out there "null", "", and sending nothing at all are three very different things to a server. The compiler cannot help you at that border.

Incremental migration is right, but do not make it mechanical. For every module I moved, I re-read how the data flowed rather than only translating the syntax. The bug above sails straight through if you change getId() to ?.id and call it done.

And one decision I am glad about: I left the architecture alone. MVP stayed MVP; only the language moved. Stacking two large changes at once in an app people use daily makes every bug hard to trace — is it the language, or the architecture? One change at a time.