Nullable and Default
One question that frequently arises when dealing with JSON, and to a limited extend CBOR as well, is how to deal with null
values.
null
values differ from missing members (see also Map-Based Codecs) in that the value for an element is indeed present, but is null
.
borer handles this case in a properly typed fashion: If your data model allows for certain members to have a null
encoding that you would like to treat specially the member’s type should be wrapped with Nullable
, e.g. Nullable[String]
.
In combination with the simple type class Default[T]
, which provides the capability to supply default values for a type T
, the pre-defined Encoder and Decoder for Nullable[T]
will be able to translate null
values to the respective default value and back.
Example:
sourceimport io.bullet.borer.{Json, Codec, Nullable}
import io.bullet.borer.derivation.MapBasedCodecs.*
case class Dog(age: Int, name: Nullable[String]) derives Codec
Json
.decode("""{ "age": 4, "name": null }""" getBytes "UTF8")
.to[Dog]
.value ==>
Dog(age = 4, name = "") // the `Default[String]` provides an empty String
NullOptions
Sometimes it’s convenient to map “nullable” fields to Options, i.e. null
to None
and non-null to Some
. This can easily be done with this import:
import io.bullet.borer.NullOptions.given
Here is how NullOptions
are implemented:
source/**
* In order to enable an alternative [[Option]] codec, which
* encodes `None` to `null` and `Some` to an unwrapped value
* you can import the members of this object with
*
* {{{
* import io.bullet.borer.NullOptions.given
* }}}
*/
object NullOptions:
given encoder[T: Encoder]: Encoder[Option[T]] =
Encoder {
case (w, Some(x)) => w.write(x)
case (w, None) => w.writeNull()
}
given decoder[T: Decoder]: Decoder[Option[T]] =
Decoder { r =>
if (r.tryReadNull()) None
else Some(r.read[T]())
}
At first glance it might seem that NullOptions
could actually be the default way to encode a value of type Option[T]
since null
appears as the “natural” construct in JSON / CBOR to represent “no value”.
However, NullOptions
have a serious drawback which makes them unsuitable as the generally preferred representation strategie for Option[T]
:
They are unsound as in “they don’t compose”.
Consider the type Option[Option[T]]
. With NullOptions
its value Some(None)
would serialize to null
, which would then deserialize to None
rather than Some(None)
, which is unsound as it violates the basic “roundtrip requirement” deserialize(serialize(x)) == x
.