The dangers of JSON + default values

The following code converts an object to JSON and then back again:

val adapter = Moshi.Builder().build().adapter(

val foo = Foo(bar = null)
val json = adapter.toJson(foo)
val foo2 = adapter.fromJson(json)

check(foo == foo2)

I think most of us would expect check() to pass, but there's actually one circumstance where it will fail: a nullable field with a non-null default value.

data class Foo(val bar: String? = "oops")

Because we're mixing nullability + non-null default values...

  • toJson(Foo(null)) == "{}" because there's no reason to put a null bar in the output.
  • fromJson("{}") == Foo("oops") because there's a default value set for bar.

This lack of symmetry can be a subtle trap, especially if you are storing JSON anywhere. Suppose that Foo did not used to have a default value - in that case, {} is a valid representation of Foo, but adding a default value later changes its meaning.

The best solution is to avoid the problem altogether by not mixing non-null default values with nullability. A property should either be nullable OR have a non-null default value, but not both.

In the above case, if bar were non-null, then its value would always be present when serializing to JSON. Alternatively, if there were no default value, then parsing the JSON would not result in any surprises - regardless of an absent key vs. null value, you'd end up with a null result.

Sometimes you will want nullability + default values (for example, you want to be able to tell the difference between an explicitly null value vs. an absent key). In that case, you can instead serialize nulls. By doing so, we make it explicit that a field is null:

val buffer = Buffer()
val writer = JsonWriter.of(buffer)
writer.serializeNulls = true
adapter.toJson(writer, foo)
val json = buffer.readUtf8()

Now, when we serialize to JSON, we are always explicitly defining bar's value, even if it is null. Thus the default value won't come into play when parsing the JSON.

Theoretically, a library could automatically handle this for you by serializing nulls if it ran into a null value on a property with a non-null default value. I've written up a request on Moshi for just such a feature, we'll see where it goes.

While I've written my examples in Moshi, this dilemma could pop up with any JSON library that handles default values, so beware.

Many thanks to Zac Sweers for reviewing this post.