Polymorphically reading JSON with Json4s and custom serializer - json

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.

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)
}

Play Framework 2.5 Serializing JSON Traits

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")
}

RuntimeException on validation of JsonReaders - Scala - ReactiveMongo

I have a case classe to store my userOption that I insert into my User.
The structure of my user is as such:
{
"_id":ObjectId("55d54d05ece39a6cf774c3e4"),
"main":{
"providerId":"userpass",
"userId":"test1#email.com",
"firstName":"test",
"lastName":"one",
"fullName":"Test One",
"email":"test1#email.com",
"authMethod":{
"method":"userPassword"
},
"passwordInfo":{
"hasher":"bcrypt",
"password":"aslkdjfasdjh"
}
},
"userOption":{
"hotLeadNotification":{
"f":"IMMEDIATE"
}
}
}
now, I'd like to add an additional option: favoriteNotification.
I changed my case class adding favoriteNotification:
case class UserOption (
hotLeadNotification: Frequency = Frequency("IMMEDIATE"),
favoriteNotification: Frequency = Frequency("IMMEDIATE")
)
object UserOption{
implicit val userOptionFormat = Json.format[UserOption]
implicit object BSONObjectIDFormat extends Format[BSONObjectID] {
def writes(objectId: BSONObjectID): JsValue = JsString(objectId.toString())
def reads(json: JsValue): JsResult[BSONObjectID] = json match {
case JsString(x) => {
val maybeOID: Try[BSONObjectID] = BSONObjectID.parse(x)
if(maybeOID.isSuccess) JsSuccess(maybeOID.get) else {
JsError("Expected BSONObjectID as JsString")
}
}
case _ => JsError("Expected BSONObjectID as JsString")
}
}
val userOptionForm = Form(
mapping(
"hotLeadNotification" -> text,
"favoriteNotification" -> text
)((hotLeadNotification: String, favoriteNotification: String) =>
UserOption(
hotLeadNotification = Frequency(hotLeadNotification),
favoriteNotification = Frequency(favoriteNotification)
)
)((u:UserOption) => Some(u.hotLeadNotification.f, u.favoriteNotification.f))
)
implicit object UserOptionBSONReader extends BSONDocumentReader[UserOption] {
def read(doc: BSONDocument): UserOption =
UserOption(
doc.getAs[Frequency]("hotLeadNotification").getOrElse(Frequency("IMMEDIATE")),
doc.getAs[Frequency]("favoriteNotification").getOrElse(Frequency("IMMEDIATE"))
)
}
implicit object UserOptionBSONWriter extends BSONDocumentWriter[UserOption]{
def write(userOption: UserOption): BSONDocument =
BSONDocument(
"hotLeadNotification" -> userOption.hotLeadNotification,
"favoriteNotification" -> userOption.favoriteNotification
)
}
}
Since I added favoriteNotification, I get a RuntimeException:
java.lang.RuntimeException: (/userOption/favoriteNotification,List(ValidationError(error.path.missing,WrappedArray())))
at scala.sys.package$.error(package.scala:27) ~[scala-library-2.11.6.jar:na]
at play.api.libs.iteratee.Iteratee$$anonfun$run$1.apply(Iteratee.scala:355) ~[play-iteratees_2.11-2.3.9.jar:2.3.9]
at play.api.libs.iteratee.Iteratee$$anonfun$run$1.apply(Iteratee.scala:348) ~[play-iteratees_2.11-2.3.9.jar:2.3.9]
at play.api.libs.iteratee.StepIteratee$$anonfun$fold$2.apply(Iteratee.scala:670) ~[play-iteratees_2.11-2.3.9.jar:2.3.9]
at play.api.libs.iteratee.StepIteratee$$anonfun$fold$2.apply(Iteratee.scala:670) ~[play-iteratees_2.11-2.3.9.jar:2.3.9]
But there's not list in my code. What am I doing wrong?
Thanks for your help
The issue was that UserOption was an Option, but not it parameters. As I added only the new option is the case class and not in the database, it was throwing this error.
I changed the case class as adding options:
case class UserOption (
hotLeadNotification: Option[Frequency] = Some(Frequency("IMMEDIATE")),
favoriteNotification: Option[Frequency] = Some(Frequency("IMMEDIATE"))
)

What should a Play framework implicit val Writes[T] look like for super type?

What do I put instead of ??? so the code will type check? Or is there something else I should be doing? I'm using Play to generate JSON for classes B, C, D that all extend A (Layer), but the code that tries to build the JSON only knows it has an A, not which subtype B, C or D.
class Layer
object Layer {
implicit val layerWrites = new Writes[Layer] {
def writes(x: Layer) = x match {
case a: CloudLayer => ???
case b: VerticalVisibility => ???
case c: SkyClear => ???
}
}
}
case class CloudLayer(coverage: String, base: Int) extends Layer
case class VerticalVisibility(height: Int) extends Layer
case class SkyClear() extends Layer
object CloudLayer {
implicit val cloudLayerWrites = new Writes[CloudLayer] {
def writes(x: CloudLayer) = Json.obj(
"layerType" -> "cloudLayer",
"coverage" -> x.cloudCoverage,
"base" -> x.base * 100
)
}
}
object VerticalVisibility {
implicit val verticalVisibilityWrites = new Writes[VerticalVisibility] {
def writes(x: VerticalVisibility) = Json.obj(
"layerType" -> "verticalVisibility",
"height" -> x.height * 100
)
}
}
object SkyClear {
implicit val skyClearWrites = new Writes[SkyClear] {
def writes(x: SkyClear) = Json.obj( "layerType" -> "skyClear" )
}
}
The easiest solution would be just to remove the implicit modifiers from the instances in the subclasses and then refer to them explicitly:
object Layer {
implicit val layerWrites = new Writes[Layer] {
def writes(x: Layer) = x match {
case a: CloudLayer => CloudLayer.cloudLayerWrites.writes(a)
case b: VerticalVisibility =>
VerticalVisibility.verticalVisibilityWrites.writes(b)
case c: SkyClear => SkyClear.skyClearWrites.writes(c)
}
}
}
You could also just scrap the individual instances and move their contents into the pattern match.
If you're feeling adventurous, Julien Richard-Foy has a pretty neat enhanced version of the Json.writes, etc. macros that works on sealed type hierarchies.

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!