Web Service Data Models - Null
This relates to my other post “Typed Actions In Play”. You may want to skim it to understand my aim in all this madness.
I’m on quest. I am attempting to create some abstractions that allow me to work solely with well-typed models for the purpose of developing web services. I am primarily working in JSON which means I just need to come up with a sound data-model for the JSON that I expect to send/receive to/from the web-service clients. Sounds like a fairly easy task, yes? Well, sadly it is not.
Living with Null
Null values in JSON are a very interesting thing to represent in Scala. While Scala
does have the concept of null
, it seems primarily as a way to survive interop with
Java, where using null
is very common. However the conventional wisdom in the Scala
community is to replace null
with Option[T]
for some type T
that is null
‘able.
T
can then be represented as Some[T]
or None
. This makes a lot of sense, but it
fails to meet the requirements for JSON in the context of web services, and we’ll see
why.
Let’s assume that we’re going to build a service to manage users. The JSON to represent a user may look like this
1
2
3
4
5
{
"name": "John Murray",
"email": "[email protected]",
"userName": "johnmurray_io"
}
Let’s assert that the userName
can be, in the JSON, either a string value or a null
value. Let’s further assert that if the user does not provide this in the POST
JSON
that it defaults to a null
value.
For editing the object, let’s assume that we accept partial edits over PUT
and/or
PATCH
(depending on which philosophy you subscribe to). This means that I do not
have to supply the full JSON object, only the fields that I wish to edit. This is a
very common practice in web services.
If I were only looking at the requirements for object creation, then I might define my model like so
1
case class User(name: String, email: String, userName: Option[String])
and the previous data would translate into
1
User("John Murray", "[email protected]", Some("johnmurray_io"))
However, this model does not meet all of our requirements, only those for object creation. Specifically what happens when I send in the following, partial update
1
2
3
4
{
"name": "John M. Murray",
"email": "[email protected]"
}
This translates into our model as being
1
User("John M. Murray", "[email protected]", None)
You can easily see where the breakdown is. Because we do not have a value for the userName
it has no other choice than to be None
. We are conflating the absence of a value with
the value being null
. In the case that the value is absent on an edit operation, the user
does not intend to edit the field. This means that we cannot reliably set the userName
field to an empty value, ever. To solve this we can introduce the TriState[T]
type.
Introducing the TriState[T]
As we have seen, we are in need of a type that can represent more than the two states
provided by Option[T]
. To handle this, we’re going to introduce a new type called
TriState[T]
whose name we’re borrowing from the world of electrical engineering. See
this article on three-state-logic for more details.
1
2
3
4
5
6
trait TriState[T]
object TriState {
case class Present[T](value: T) extends TriState[T]
case class Absent extends TriState[Nothing]
case class Null extends TriState[Nothing]
}
If we now replace the type of userName
of Option[T]
with TriState[T]
, we can properly
represent the edit JSON in our model as:
1
User("John M. Murray", "[email protected]", TriState.Absent)
From this, we know not to edit the field, since it is absent. Additionally if I were to
provide a null value on edit, we would know to set the userName
to null
/empty within
our persistent storage solution. From this we can see that Option
, for web services, is
better replaced with a TriState
.