Combining Writes
Introduction
We've already explained what a Write
is in the previous chapter. Those examples were only covering simple writes. Most of the time, writes are used to transform complex hierarchical objects.
In the validation API, we create complex object writes by combining simple writes. This chapter details the creation of those complex writes.
All the examples below are transforming classes to Json objects. The API is not dedicated only to Json, it can be used on any type. Please refer to Serializing Json, Serializing Forms, and Supporting new types for more information.
Path
Serializing data using Path
The write
method
We start by creating a Path representing the location at which we'd like to serialize our data:
import jto.validation._
val location: Path = Path \ "user" \ "friend"
Path
has a write[I, O]
method, where I
represents the input we’re trying to serialize, and O
is the output type. For example, (Path \ "foo").write[Int, JsObject]
, means we want to try to serialize a value of type Int
into a JsObject
at /foo
.
But let's try something much easier for now:
scala> import jto.validation._
import jto.validation._
scala> import play.api.libs.json._
import play.api.libs.json._
scala> val location: Path = Path \ "user" \ "friend"
location: jto.validation.Path = /user/friend
scala> val serializeFriend: Write[JsValue, JsObject] = location.write[JsValue, JsObject]
<console>:22: error: No implicit view available from jto.validation.Path => jto.validation.WriteLike[play.api.libs.json.JsValue,play.api.libs.json.JsObject].
val serializeFriend: Write[JsValue, JsObject] = location.write[JsValue, JsObject]
^
location.write[JsValue, JsObject]
means the we're trying to serialize a JsValue
to location
in a JsObject
. Effectively, we're just defining a Write
that is putting a JsValue
into a JsObject
at the given location.
If you try to run that code, the compiler gives you the following error:
scala> val serializeFriend: Write[JsValue, JsObject] = location.write[JsValue, JsObject]
<console>:22: error: No implicit view available from jto.validation.Path => jto.validation.WriteLike[play.api.libs.json.JsValue,play.api.libs.json.JsObject].
val serializeFriend: Write[JsValue, JsObject] = location.write[JsValue, JsObject]
^
The Scala compiler is complaining about not finding an implicit function of type Path => Write[JsValue, JsObject]
. Indeed, unlike the Json API, you have to provide a method to transform the input type into the output type.
Fortunately, such method already exists. All you have to do is import it:
import jto.validation.playjson.Writes._
By convention, all useful serialization methods for a given type are to be found in an object called
Writes
. That object contains a bunch of implicits defining how to serialize primitives Scala types into the expected output types.
With those implicits in scope, we can finally create our Write
:
val serializeFriend: Write[JsValue, JsObject] = location.write[JsValue, JsObject]
Alright, so far we've defined a Write
looking for some data of type JsValue
, located at /user/friend
in a JsObject
.
Now we need to apply this Write
on our data:
scala> serializeFriend.writes(JsString("Julien"))
res0: play.api.libs.json.JsObject = {"user":{"friend":"Julien"}}
Type coercion
We now are capable of serializing data to a given Path
. Let's do it again on a different sub-tree:
val agejs: Write[JsValue, JsObject] = (Path \ "user" \ "age").write[JsValue, JsObject]
And if we apply this new Write
:
scala> agejs.writes(JsNumber(28))
res1: play.api.libs.json.JsObject = {"user":{"age":28}}
That example is nice, but chances are age
in not a JsNumber
, but an Int
.
All we have to do is to change the input type in our Write
definition:
val age: Write[Int, JsObject] = (Path \ "user" \ "age").write[Int, JsObject]
And apply it:
scala> age.writes(28)
res2: play.api.libs.json.JsObject = {"user":{"age":28}}
So scala automagically figures out how to transform a Int
into a JsObject
. How does this happen?
It's fairly simple. The definition of write
looks like this:
def write[I, O](implicit w: Path => Write[I, O]): Write[I, O] = ???
So when you use (Path \ "user" \ "age").write[Int, JsObject]
, the compiler looks for an implicit Path => Write[Int, JsObject]
, which happens to exist in jto.validation.json.Writes
.
Full example
import jto.validation._
import jto.validation.playjson.Writes._
import play.api.libs.json._
val age: Write[Int, JsObject] = (Path \ "user" \ "age").write[Int, JsObject]
scala> age.writes(28)
res4: play.api.libs.json.JsObject = {"user":{"age":28}}
Combining Writes
So far we've serialized only primitives types.
Now we'd like to serialize an entire User
object defined below, and transform it into a JsObject
:
case class User(
name: String,
age: Int,
email: Option[String],
isAlive: Boolean
)
We need to create a Write[User, JsValue]
. Creating this Write
is simply a matter of combining together the writes serializing each field of the class.
import jto.validation._
import jto.validation.playjson.Writes._
import play.api.libs.json._
import scala.Function.unlift
val userWrite: Write[User, JsObject] = To[JsObject] { __ =>
import jto.validation.playjson.Writes._
((__ \ "name").write[String] ~
(__ \ "age").write[Int] ~
(__ \ "email").write[Option[String]] ~
(__ \ "isAlive").write[Boolean])(unlift(User.unapply))
}
Important: Note that we're importing
Writes._
inside theTo[I]{...}
block. It is recommended to always follow this pattern, as it nicely scopes the implicits, avoiding conflicts and accidental shadowing.
To[JsObject]
defines the O
type of the writes we're combining. We could have written:
(Path \ "name").write[String, JsObject] ~
(Path \ "age").write[Int, JsObject] ~
//...
but repeating JsObject
all over the place is just not very DRY.
Let's test it now:
scala> userWrite.writes(User("Julien", 28, None, true))
res6: play.api.libs.json.JsObject = {"name":"Julien","age":28,"isAlive":true}