Play Framework 2.5 Serializing JSON Traits - json

I have a sealed trait that is like below:
sealed trait MyMessages
object MyMessages {
case object Init extends MyMessages
case object Destroy extends MyMessages
case class Tick(elem: Long) extends MyMessages
}
I have to now write a formatter for serializing and de-serializing this into to and from a JSON. This is what I came up with:
implicit object MyMessagesWrites extends Writes[MyMessages] {
def writes(myMessages: MyMessages): JsValue = myMessages match {
case Init => Json.toJson("INIT")
case Destroy => Json.toJson("DESTROY")
case tick: Tick => Json.toJson(Tick)
}
def reads(json: JsValue): MyMessages = {
// How do I get from JSValue to a MyMessages type???
}
}
Implementing the writes was easy, but how do I implement the reads?

Assuming you serialize the Tick instance as a bare JSON number, I would do it like so:
implicit object MyMessageReads extends Reads[MyMessages] {
def reads(json: JsValue) = json match {
case JsString("INIT") => JsSuccess(MyMessages.Init)
case JsString("DESTROY") => JsSuccess(MyMessages.Destroy)
case JsNumber(n) => JsSuccess(Tick(n.toLongExact))
case e => JsError(s"Invalid message: $e")
}
}
Note that you can also make the reads/writes a bit more succinct by using the more functional style:
implicit val myMessagesWrites = Writes[MyMessages] {
case Init => JsString("INIT")
case Destroy => JsString("DESTROY")
case Tick(n) => JsNumber(n)
}
implicit val myMessageReads = Reads[MyMessages] {
case JsString("INIT") => JsSuccess(MyMessages.Init)
case JsString("DESTROY") => JsSuccess(MyMessages.Destroy)
case JsNumber(n) => JsSuccess(Tick(n.toLongExact))
case e => JsError(s"Invalid message: $e")
}

Related

Json body converted to sealed trait

I have a Play! endpoint which can receive a json body as 3 or 4 forms (I tried using generic type, but not working).
Controller:
def getChartData = Action.async(parse.json) { request =>
// ERROR: can not cast JsValue to type ChartDataRequest (which is sealed trait)
service.getChartData(request.body.asInstanceOf[ChartDataRequest]).map {
data => Ok(Json.toJson(data))
}.recover {
case _ => InternalServerErrror
}
}
Service:
def getChartData(request: ChartDataRequest): Future[data_type] = {
if (request.isInstanceOf[PieChartRequest]) {
//
} else if (request.isInstanceOf[BarChartRequest]) {
//
} else {
//
}
}
dtos:
sealed trait ChartDataRequest
final case class PieChartRequest(field1: String, field2: String, ....)
extends ChartDataRequest
final case class BarChartRequest(field1: String, field2: String, ....)
extends ChartDataRequest
I found here the solution to use sealed traits, but can't do it well.
In this point, I can not convert the JsValue to ChartDataRequest type. I can use a field "classType" in my json and then using the match pattern to create the specified object (PieDataRequest or BarDataRequest) but I think this is not the best solution.
Inside all my controller methods where I send objects as json body, I use the play validator, but have the same problem, and I removed it from code.
// ChartDataRequest can have PieDataRequest or BarDataRequest type
request.body.validate[ChartDataRequest] match {
case JsSuccess(value, _) => // call the service
case JsError(_) => Future(BadRequest("Invalid json body"))
}
thanks
You can follow this:
sealed trait ChartDataRequest
final case class PieChartRequest(field1: String) extends ChartDataRequest
final case class BarChartRequest(field2: String) extends ChartDataRequest
final case object WrongData extends ChartDataRequest
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val ChartDataRequests: Reads[ChartDataRequest] = {
val pc = Json.reads[PieChartRequest]
val bc = Json.reads[BarChartRequest]
__.read[PieChartRequest](pc).map(x => x: ChartDataRequest) |
__.read[BarChartRequest](bc).map(x => x: ChartDataRequest)
}
def getChartData(request: ChartDataRequest) = {
request match {
case _: PieChartRequest =>
Future("PieChartRequest")(defaultExecutionContext)
case _: BarChartRequest =>
Future("BarChartRequest")(defaultExecutionContext)
case _ =>
Future("WrongData")(defaultExecutionContext)
}
}
def getChartDataAction = Action.async(parse.json) { request =>
// you can separate this to a new function
val doIt = request.body.asOpt[JsObject].fold[ChartDataRequest](
WrongData
){
jsObj =>
jsObj.asOpt[ChartDataRequest].fold[ChartDataRequest](
WrongData
)(identity)
}
getChartData(doIt).map {
data => Ok(Json.toJson(data))
}(defaultExecutionContext).recover {
case _ => InternalServerError
}(defaultExecutionContext)
}

How to implement custom deserializer for type Boolean in spray json

I have a couple of Boolean attributes in my API model and would like to accept true/false as well as 1/0 values. My first idea was to implement custom formatter:
object UserJsonProtocol extends DefaultJsonProtocol {
implicit object MyBooleanJsonFormat extends JsonFormat[Boolean] {
def write(value: Boolean): JsString = {
return JsString(value.toString)
}
def read(value: JsValue) = {
value match {
case JsString("1") => true
case JsString("0") => false
case JsString("true") => true
case JsString("false") => false
case _ => throw new DeserializationException("Not a boolean")
}
}
}
implicit val userFormat = jsonFormat15(User.apply)
}
where User is a model with Boolean attributes. Unfortunately above solution has no effect - 1/0 are not accepted as Booleans. Any solution?
After fixing some issues with types and pattern matching it seems to work:
implicit object MyBooleanJsonFormat extends JsonFormat[Boolean] {
def write(value: Boolean): JsBoolean = {
return JsBoolean(value)
}
def read(value: JsValue) = {
value match {
case JsNumber(n) if n == 1 => true
case JsNumber(n) if n == 0 => false
case JsBoolean(true) => true
case JsBoolean(false) => false
case _ => throw new DeserializationException("Not a boolean")
}
}
}

How to marshal different response types in spray?

Consider an http service that can return two json as response:
successful
{
"yourField":"value"
}
failure
{
"errorCode": 3
}
To deal with these json's I need to create 2 case classes case class RespSucc(yourField:String) and
case class RespFail(errorCode:Int).
For now I have to to something like that:
//unmarshal is spray.httpx.ResponseTransformation#unmarshal
if (response.entity.asString.contains("errorCode")) {
unmarshal[RespSucc].apply(response)
}
else {
unmarshal[RespFail].apply(response)
}
Is there an api to parse these classes automatically without any if? E.g. can unmarshaller looks into json fields and select approriate case class?
spray-json supports Either which is a very useful data type for this kind of situations.
val data = unmarshal[Either[RespFail, RespSucc]].apply(response)
// You can match it
data match {
case Left(err) => handleError(err)
case Right(suc) => handleSuccess(suc)
}
// Or you can fold it (I prefer folding)
data.fold(err => handleError(err), suc => handleSuccess(suc))
You can try something like this:
trait Resp
case class RespSucc(yourField: String) extends Resp
case class RespFail(errorCode: Int) extends Resp
object MyJsonProtocol extends DefaultJsonProtocol {
implicit object ColorJsonFormat extends RootJsonFormat[Resp] {
def write(r: Resp) = r match {
case s: RespSucc =>
JsObject("yourField" -> JsString(s.yourField))
case f: RespFail =>
JsObject("errorCode" -> JsNumber(f.errorCode))
}
def read(value: JsValue) = value.asJsObject.getFields("yourField", "errorCode") match {
case Seq(JsString(yourField)) => RespSucc(yourField)
case Seq(JsNumber(errorCode)) => RespFail(errorCode.intValue())
case _ => deserializationError("Resp expected")
}
}
}
import MyJsonProtocol._
unmarshal[Resp](entitySucc) //Right(RespSucc(abc))
unmarshal[Resp](entityFail) //Right(RespFail(3))

Spray: Marshalling UUID to JSON

I'm having some problems marshalling from UUID to JSON
def complete[T <: AnyRef](status: StatusCode, obj: T) = {
r.complete(status, obj) // Completes the Request with the T obj result!
}
^
The signature of my class:
trait PerRequest extends Actor
with Json4sSupport
with Directives
with UnrestrictedStash
with ActorLogging {
val json4sFormats = DefaultFormats.
This gives me :
"id": {
"mostSigBits": -5052114364077765000,
"leastSigBits": -7198432257767597000
},
instead of:
"id": "b9e348c0-cc7f-11e3-9c1a-0800200c9a66"
So, how can I add a UUID format to json4sFormats to marshall UUID's correctly?? In other cases I mix in with a trait that have this function:
implicit object UuidJsonFormat extends RootJsonFormat[UUID] {
def write(x: UUID) = JsString(x.toString)
def read(value: JsValue) = value match {
case JsString(x) => UUID.fromString(x)
case x => deserializationError("Expected UUID as JsString, but got " + x)
}
}
But here I'm not able to because I don't have declared a spray.json.RootJsonReader and/or spray.json.RootJsonWriter for every type T and does not compile. (See complete function T <: AnyRef)
Thanks.
I solved it! If someone has the same problem take a look here
I defined my own UUID Serializer as follows:
class UUIDSerializer extends CustomSerializer[UUID](format => (
{
case JObject(JField("mostSigBits", JInt(s)) :: JField("leastSigBits", JInt(e)) :: Nil) =>
new UUID(s.longValue, e.longValue)
},
{
case x: UUID => JObject(JField("id", JString(x.toString)))
}
))
And now it's working!

Polymorphically reading JSON with Json4s and custom serializer

I have the following class hierachy:
object Calendar {
trait DayType
case object Weekday extends DayType
case object Weekend extends DayType
case object Holiday extends DayType
}
trait Calendar {
def dateType(date: LocalDate): Calendar.DayType
}
class ConstantCalendar(dayType: Calendar.DayType) extends Calendar {
override def dateType(date: LocalDate) = dayType
}
case object DefaultCalendar extends ConstantCalendar(Calendar.Weekday)
case class WeekdaysCalendar(defaults: Array[Calendar.DayType]) extends Calendar {
override def dateType(date: LocalDate) = defaults(date.getDayOfWeek - 1)
}
case class CustomCalendar(defaultCalendar: Calendar = DefaultCalendar,
dates: Map[LocalDate, Calendar.DayType] = Map.empty)
extends Calendar {
private def defaultType(date: LocalDate) = defaultCalendar.dateType(date)
private val dateMap = dates.withDefault(defaultType)
override def dateType(date: LocalDate) = dateMap(date)
}
I have defined the following serializers:
class JsonFormats(domainTypeHints: TypeHints,
domainCustomSerializers: List[Serializer[_]] = Nil,
domainFieldSerializers: List[(Class[_], FieldSerializer[_])] = Nil)
extends DefaultFormats {
override val typeHintFieldName = "type"
override val typeHints = domainTypeHints
override val customSerializers = JodaTimeSerializers.all ++ domainCustomSerializers
override val fieldSerializers = domainFieldSerializers
}
class JsonCalendarSerializer extends CustomSerializer[CustomCalendar]( format => (
{
case JObject(JField("type", JString("CustomCalendar")) ::
JField("defaultCalendar", JString(defaultCalendar)) ::
JField("dates", dates) ::
Nil
) =>
CustomCalendar(defaultCalendar) // TODO dates
},
{
case cal: CustomCalendar =>
val dates = cal.dates.foldLeft(JObject()) { (memo, dt) =>
dt match {
case (d, t) => memo ~ (f"${d.getYear}%04d-${d.getMonthOfYear}%02d-${d.getDayOfMonth}%02d", t.toString)
}
}
("type" -> "CustomCalendar") ~
("defaultCalendar" -> cal.defaultCalendar) ~
("dates" -> dates)
}
))
implicit val jsonFormats = new JsonFormats(ShortTypeHints(List(Calendar.Weekday.getClass,
Calendar.Weekend.getClass,
Calendar.Holiday.getClass,
classOf[CustomCalendar])),
new JsonCalendarSerializer :: Nil)
I had to create a custom Serializer to get around the fact that, in Json4s, Map keys have to be Strings.
I have a file that might contain the data for some Calendar, but I don't know beforehand which Calendar type it is.
When I try the following:
val cal = CustomCalendar("default", Map(new LocalDate(2013, 1, 1) -> Calendar.Holiday))
val ser = Serialization.write(cal)
val cal2: Calendar = Serialization.read(ser)
I get:
org.json4s.package$MappingException: Do not know how to deserialize 'CustomCalendar'
at org.json4s.Extraction$ClassInstanceBuilder.org$json4s$Extraction$ClassInstanceBuilder$$mkWithTypeHint(Extraction.scala:444)
at org.json4s.Extraction$ClassInstanceBuilder$$anonfun$result$6.apply(Extraction.scala:452)
at org.json4s.Extraction$ClassInstanceBuilder$$anonfun$result$6.apply(Extraction.scala:450)
at org.json4s.Extraction$.org$json4s$Extraction$$customOrElse(Extraction.scala:462)
at org.json4s.Extraction$ClassInstanceBuilder.result(Extraction.scala:450)
at org.json4s.Extraction$.extract(Extraction.scala:306)
at org.json4s.Extraction$.extract(Extraction.scala:42)
at org.json4s.ExtractableJsonAstNode.extract(ExtractableJsonAstNode.scala:21)
at org.json4s.jackson.Serialization$.read(Serialization.scala:50)
So it seems that Json4s isn't able to find my serializer.
So... any hints? Either on how to get Json4s to serialize/deserialize Maps with non-String keys, or how to make this work?
Thanks!
In the end I implemented the JsonCalendarSerializer as follows:
class JsonCalendarSerializer extends CustomSerializer[CustomCalendar]( format => (
{
case JObject(JField("defaults", JString(defaults)) ::
JField("dates", JObject(dateList)) ::
Nil
) =>
val dates = dateList map {
case JField(dt, JString(t)) =>
val tp = t match {
case "Weekday" => Calendar.Weekday
case "Weekend" => Calendar.Weekend
case "Holiday" => Calendar.Holiday
}
(LocalDate.parse(dt), tp)
}
CustomCalendar(defaults, dates.toMap)
},
{
case cal: CustomCalendar =>
val dates = cal.dates.foldLeft(JObject()) { (memo, dt) =>
dt match {
case (d, t) => memo ~ (d.toString, t.toString)
}
}
(format.typeHintFieldName -> classOf[CustomCalendar].getSimpleName) ~
("defaults" -> cal.defaultCalendar) ~
("dates" -> dates)
}
))
I removed the JField("type"...) from the deserializer and fixed the serializer to call format.typeHintFieldName and classOf[CustomCalendar].getSimpleName, and that seemed to fix the problem.