In Part 1 article, we gave a quick overview of new Play2.1 JSON API features around JsPath and Reads combinators. It’s funny to be able to read JSON to Scala structures but it’s better to be able to write Scala structures to JSON (
Writes[T]
) and it’s even better to mix Reads and Writes (Format[T]
) sometimes.
Now let’s focus on Writes and Format in the details ;)
Writes[T] hasn’t changed (except combinators)
Writes in Play2.0.x
Do you remember how you had to write a Json Writes[T]
in Play2.0.x
?
You had to override the writes
function.
1 2 3 4 5 6 7 |
|
Take the same simple case class we used in Part 1:
1 2 3 4 5 |
|
In Play2.0.x
, you would write your Writes[Creature]
as following (using new Json syntax to re-show it even if it didn’t exist in Play2.0.x ;) ):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Writes in Play2.1.x
No suspense to be kept: in Play2.1, you write Writes exactly in the same way :D
So what’s the difference?
As presented in Part 1, Reads
could be combined using simple logical operators.
Using functional Scala power, we were able to provide combinators for Writes[T]
.
If you want more theoretical aspects about the way it was implemented based on generic functional structures adapted to our needs, you can read this post “Applicatives are too restrictive, breaking Applicatives and introducing Functional Builders” written by @sadache
Writes main change: combinators
Once again, code first: re-writing previous Writes[T]
using combinators.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
It looks exactly like Reads[T]
except a few things, isn’t it?
Let’s explain a bit (by copying Reads article changing just a few things… I’m lazy ;)):
import play.api.libs.json.Writes._
It imports only the required stuff for Writes[T]
without interfering with other imports.
(__ \ "name").write[String]
You apply write[String]
on this JsPath (exactly the same as Reads
)
and
is just an operator meaning Writes[A] and Writes[B] => Builder[Writes[A ~ B]]
A ~ B
just meansCombine A and B
but it doesn’t suppose the way it is combined (can be a tuple, an object, whatever…)Builder
is not a real type but I introduce it just to tell that the operatorand
doesn’t create directly aWrites[A ~ B]
but an intermediate structure that is able to build aWrites[A ~ B]
or to combine with anotherWrites[C]
(…)(unlift(Creature.unapply))
builds a Writes[Creature]
(__ \ "name").write[String] and (__ \ "isDead").write[Boolean] and (__ \ "weight").write[Float]
builds aBuilder[Writes[String ~ Boolean ~ Float])]
but you want aWrites[Creature]
.- So you apply the
Builder[Writes[String ~ Boolean ~ String])]
to a functionCreature => (String, Boolean, Float)
to finally obtain aWrites[Creature]
. Please note that it may seem a bit strange to provideCreature => (String, Boolean, Float)
to obtain aWrites[Creature]
from aBuilder[Writes[String ~ Boolean ~ String])]
but it’s due to the contravariant nature ofWrites[-T]
. - We have
Creature.unapply
but its signature isCreature => Option[(String, Boolean, Float)]
so weunlift
it to obtainCreature => (String, Boolean, Float)
.
The only thing you have to keep in mind is this
unlift
call which might not be natural at first sight!
As you can deduce by yourself, the Writes[T]
is far easier than the Reads[T]
case because when writing, it doesn’t try to validate so there is no error management at all.
Moreover, due to this, you have to keep in mind that operators provided for Writes[T]
are not as rich as for Reads[T]
. Do you remind keepAnd
and andKeep
operators? They don’t have any meaning for Writes[T]
. When writing A~B
, you write A and B
but not only A or only B
. So and
is the only operators provided for Writes[T]
.
Complexifying the case
Let’s go back to our more complex sample used in end of Part1. Remember that we had imagined that our creature was modelled as following:
1 2 3 4 5 6 7 8 9 |
|
Let’s write corresponding Writes[Creature]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
You can see that it’s quite straightforward. it’s far easier than Reads[T]
as there are no special operator.
Here are the few things to explain:
(__ \ "favorites").write(…)
(__ \ "string").write[String] and
(__ \ "number").write[Int]
tupled
- Remember that
(__ \ "string").write[String](…) and (__ \ "number").write[Int](…) => Builder[Writes[String ~ Int]]
- What means
tupled
? as forReads[T]
, it “tuplizes” your Builder:Builder[Writes[A ~ B]].tupled => Writes[(A, B)]
(__ \ "friend").lazyWrite(Writes.traversableWrites[Creature](creatureWrites))
It’s the symmetric code for lazyRead
to treat recursive field on Creature
class itself:
Writes.traversableWrites[Creature](creatureWrites)
creates aWrites[Traversable[Creature]]
passing theWrites[Creature]
itself for recursion (please note that alist[Creature]
should appear very soon ;))(__ \ "friends").lazyWrite[A](r: => Writes[A]))
:lazyWrite
expects aWrites[A]
value passed by name to allow the type recursive construction. This is the only refinement that you must keep in mind in this very special recursive case.
FYI, you may wonder why
Writes.traversableWrites[Creature]: Writes[Traversable[Creature]]
can replaceWrites[List[Creature]]
?
This is becauseWrites[-T]
is contravariant meaning: if you can write aTraversable[Creature]
, you can write aList[Creature]
asList
inheritsTraversable
(relation of inheritance is reverted by contravariance).
What about combinators for Format?
Remember in Play2.1, there was a feature called Format[T] extends Reads[T] with Writes[T]
.
It mixed Reads[T]
and Writes[T]
together to provide serialization/deserialization at the same place.
Play2.1 provide combinators for Reads[T]
and Writes[T]
. What about combinators for Format[T]
?
Let’s go back to our very simple sample:
1 2 3 4 5 |
|
Here is how you write the Reads[Creature]
:
1 2 3 4 5 6 7 8 |
|
Please remark that I didn’t use
implicit
so that there is no implicitReads[Creature]
in the context when I’ll defineFormat[T]
Here is how you write the Writes[Creature]
:
1 2 3 4 5 6 7 8 |
|
How to gather both Reads/Writes to create a Format[Creature]
?
1st way = create from existing reads/writes
You can reuse existing Reads[T]
and Writes[T]
to create a Format[T]
as following:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
2nd way = create using combinators
We have Reads and Writes combinators, isn’t it?
Play2.1 also provides Format Combinators due to the magic of functional programming (actually it’s not magic, it’s just pure functional programming;) )
As usual, code 1st:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Nothing too strange…
(__ \ "name").format[String]
It creates a format[String] reading/writing at the given JsPath
( )(Creature.apply, unlift(Creature.unapply))
To map to a Scala structure:
Reads[Creature]
requires a function(String, Boolean, Float) => Creature
Writes[Creature]
requires a functionCreature => (String, Boolean, Float)
So as Format[Creature] extends Reads[Creature] with Writes[Creature]
we provide Creature.apply
and unlift(Creature.unapply)
and that’s all folks…
More complex case
The previous sample is a bit dumb because the structure is really simple and because reading/writing is symmetric. We have:
1
|
|
In this case, you read what you write and vis versa. So you can use the very simple JsPath.format[T]
functions which build both Reads[T]
and Writes[T]
together.
But if we take our usual more complicated case class, how to write the Format[T]
?
Remind the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|
As you can see, creatureReads
and creatureWrites
are not exactly symmetric and couldn’t be merged in one single Format[Creature]
as done previously.
1
|
|
Hopefully, as done previously, we can build a Format[T]
from a Reads[T]
and a Writes[T]
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
Conclusion & much more…
So here is the end of this 2nd part.
Json combinator is cool!
Now what about transforming Json into Json???
Why would you need this?
Hmmm: why not receive JSON, validate/transform it, send it to a managing Json (Mongo for ex or others), read it again from the DB, modify it a bit and send it out…
Json “coast to coast” without any model class…
Let’s tease a bit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Next parts about Json generators/transformers ;)
Have fun…