Generic Play json Formatter - json

I have a scala application and have a case class like -
case class SR(
systemId: Option[String] = None,
x: Map[Timestamp, CaseClass1] = Map.empty,
y: Map[Timestamp, CaseClass2] = Map.empty,
y: Map[Timestamp, CaseClass3] = Map.empty
)
Now I have to provide an implicit read and write JSON format for properties x,y,z for SR case class like -
implicit val mapCMPFormat = new Format[Map[Timestamp, CaseClass1]] {
def writes(obj: Map[Timestamp, CaseClass1]): JsValue =
JsArray(obj.values.toSeq.map(Json.toJson(_)))
def reads(jv: JsValue): JsResult[Map[Timestamp, CaseClass1]] = jv.validate[scala.collection.Seq[CaseClass1]] match {
case JsSuccess(objs, path) => JsSuccess(objs.map(obj => obj.dataDate.get -> obj).toMap, path)
case err: JsError => err
}
}
And so on similarly for Y and Z, and in future, I will be adding many more properties like x,y,z in SR case class and then need to provide the formators.
So Can I get some Generic Formater that will take care for all types?

To my knowledge, a simple way to achieve this does not exists, however, to create a "default" reader for each object should not be hard to do, something like:
case class VehicleColorForAdd(
name: String,
rgb: String
)
object VehicleColorForAdd {
implicit val jsonFormat: Format[VehicleColorForAdd] = Json.formats[VehicleColorForAdd]
}
This way you have access to the implicit by simply using the object, so you could have other objects that contains this object with no problem:
case class BiggerModel(
vehicleColorForAdd: VehicleColorForAdd
)
object BiggerModel{
implicit val jsonFormat: Format[BiggerModel] = Json.format[BiggerModel]
}
Sadly, you need to do this for each class type, but you can "extend" play converters with your own, for example, this are some of my default readers:
package common.json
import core.order.Order
import org.joda.time.{ DateTime, LocalDateTime }
import org.joda.time.format.DateTimeFormat
import core.promotion.{ DailySchedule, Period }
import play.api.libs.functional.syntax._
import play.api.libs.json.Reads._
import play.api.libs.json._
import play.api.libs.json.{ JsError, JsPath, JsSuccess, Reads }
import scala.language.implicitConversions
/**
* General JSon readers and transformations.
*/
object JsonReaders {
val dateTimeFormat = "yyyy-MM-dd HH:mm:ss"
class JsPathHelper(val path: JsPath) {
def readTrimmedString(implicit r: Reads[String]): Reads[String] = Reads.at[String](path)(r).map(_.trim)
def readUpperString(implicit r: Reads[String]): Reads[String] = Reads.at[String](path)(r).map(_.toUpperCase)
def readNullableTrimmedString(implicit r: Reads[String]): Reads[Option[String]] = Reads.nullable[String](path)(r).map(_.map(_.trim))
}
implicit val localDateTimeReader: Reads[LocalDateTime] = Reads[LocalDateTime]((js: JsValue) =>
js.validate[String].map[LocalDateTime](dtString =>
LocalDateTime.parse(dtString, DateTimeFormat.forPattern(dateTimeFormat))))
val localDateTimeWriter: Writes[LocalDateTime] = new Writes[LocalDateTime] {
def writes(d: LocalDateTime): JsValue = JsString(d.toString(dateTimeFormat))
}
implicit val localDateTimeFormat: Format[LocalDateTime] = Format(localDateTimeReader, localDateTimeWriter)
implicit val dateTimeReader: Reads[DateTime] = Reads[DateTime]((js: JsValue) =>
js.validate[String].map[DateTime](dtString =>
DateTime.parse(dtString, DateTimeFormat.forPattern(dateTimeFormat))))
implicit def toJsPathHelper(path: JsPath): JsPathHelper = new JsPathHelper(path)
val defaultStringMax: Reads[String] = maxLength[String](255)
val defaultStringMinMax: Reads[String] = minLength[String](1) andKeep defaultStringMax
val rgbRegex: Reads[String] = pattern("""^#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})$""".r, "error.invalidRGBPattern")
val plateRegex: Reads[String] = pattern("""^[\d\a-zA-Z]*$""".r, "error.invalidPlatePattern")
val minOnlyWordsRegex: Reads[String] = minLength[String](2) keepAnd onlyWordsRegex
val positiveInt: Reads[Int] = min[Int](1)
val zeroPositiveInt: Reads[Int] = min[Int](0)
val zeroPositiveBigDecimal: Reads[BigDecimal] = min[BigDecimal](0)
val positiveBigDecimal: Reads[BigDecimal] = min[BigDecimal](1)
def validLocalDatePeriod()(implicit reads: Reads[Period]) =
Reads[Period](js => reads.reads(js).flatMap { o =>
if (o.startPeriod isAfter o.endPeriod)
JsError("error.startPeriodAfterEndPeriod")
else
JsSuccess(o)
})
def validLocalTimePeriod()(implicit reads: Reads[DailySchedule]) =
Reads[DailySchedule](js => reads.reads(js).flatMap { o =>
if (o.dailyStart isAfter o.dailyEnd)
JsError("error.dailyStartAfterDailyEnd")
else
JsSuccess(o)
})
}
Then, you only need to import this object to have access to all this implicit converters:
package common.forms
import common.json.JsonReaders._
import play.api.libs.json._
/**
* Form to add a model with only one string field.
*/
object SimpleCatalogAdd {
case class Data(
name: String
)
implicit val dataReads: Reads[Data] = (__ \ "name").readTrimmedString(defaultStringMinMax).map(Data.apply)
}

Related

convert json to array of scala objects using spray json

I am not much familier with spray json, but I have to convert the below json into Array[myTest]
Below is the code, but it doesnt work. It throws the following errors: How do I fix them?
Error:(19, 54) Cannot find JsonReader or JsonFormat type class for Array[A$A61.this.myTest]
lazy val converted= trainingDataRef.toJson.convertTo[Array[myTest]]
^
Error:(19, 54) not enough arguments for method convertTo: (implicit evidence$1: spray.json.JsonReader[Array[A$A61.this.myTest]])Array[A$A61.this.myTest].
Unspecified value parameter evidence$1.
lazy val converted= trainingDataRef.toJson.convertTo[Array[myTest]]
^
Error:(10, 61) could not find implicit value for evidence parameter of type spray.json.DefaultJsonProtocol.JF[Map[String,Any]]
implicit val format: RootJsonFormat[myTest] = jsonFormat3(myTest.apply)
^
Code: ^
import spray.json.DefaultJsonProtocol._
import spray.json._
case class myTest (
id: String,
classDetails: Map[String, Any],
school: Map[String, Any])
object myTest {
implicit val format: RootJsonFormat[myTest] = jsonFormat3(myTest.apply)
}
val trainingDataRef = """[{"id":"my-id","classDetails":{"sec":"2","teacher":"John"},"school":{"name":"newschool"}}]"""
println(trainingDataRef.getClass)
val converted= trainingDataRef.toJson.convertTo[Array[myTest]]
println(converted)
spray-json has good documentation, try take a look there. Basically, you have to define your case classes and implement JsonFormat for them:
import spray.json.DefaultJsonProtocol._
import spray.json._
case class ClassDetails(sec: String, teacher: String)
object ClassDetails {
implicit val format: RootJsonFormat[ClassDetails] = jsonFormat2(ClassDetails.apply)
}
case class School(name: String)
object School {
implicit val format: RootJsonFormat[School] = jsonFormat1(School.apply)
}
case class ClassInfo
(
id: String,
classDetails: ClassDetails,
school: School
)
object ClassInfo {
implicit object ClassInfoFormat extends RootJsonFormat[ClassInfo] {
def write(c: ClassInfo): JsValue = JsObject(
"id" -> JsString(c.id),
"classDetails" -> c.classDetails.toJson,
"school" -> c.school.toJson
)
def read(value: JsValue): ClassInfo = {
value.asJsObject.getFields("id", "classDetails", "school") match {
case Seq(JsString(name), details, school) =>
new ClassInfo(name, details.convertTo[ClassDetails], school.convertTo[School])
case _ => throw new DeserializationException("ClassInfo expected")
}
}
}
}
val json = """[{"id":"my-id","classDetails":{"sec":"2","teacher":"John"},"school":{"name":"newschool"}}]"""
// JSON string to case classes
val classInfos = json.parseJson.convertTo[Seq[ClassInfo]]
classInfos.zipWithIndex.foreach { case (c, idx) =>
println(s"$idx => $c")
}
println
// Seq[ClassInfo] to JSON
println(s"$classInfos: ")
println(classInfos.toJson.prettyPrint)

No instance of play.api.libs.json.Format is available for scala.Predef.Map[java.lang.String, scala.Option[scala.Double]]

Trying to write a json format for an entity which contains a Map of Option. It throws following error
Error:(8, 68) No instance of play.api.libs.json.Format is available for scala.Predef.Map[java.lang.String, scala.Option[scala.Double]] in the implicit scope (Hint: if declared in the same file, make sure it's declared before)
Code snippet:
import play.api.libs.json.{Json, OFormat}
val a: Map[String, Option[Double]] = Map("a" -> None)
case class Person(x: Map[String, Option[Double]])
object Person {
implicit val personFormat: OFormat[Person] = Json.format[Person]
}
Json.toJson(Person(a))
You can define implicits:
import scala.collection.Map
object Person {
implicit object MapReads extends Reads[Map[String, Option[Double]]] {
def reads(jsValue: JsValue): JsResult[Map[String, Option[Double]]] = jsValue match {
case JsObject(map) => JsSuccess(
map.mapValues {
case JsNumber(d) => Some(d.toDouble)
case _ => None
}
)
case _ => JsError()
}
}
implicit object MapWrites extends Writes[Map[String, Option[Double]]] {
def writes(map: Map[String, Option[Double]]): JsValue =
JsObject(map.mapValues(optd => JsNumber(optd.getOrElse[Double](0.0))))
}
implicit val personFormat: OFormat[Person] = Json.format[Person]
}
println(Json.toJson(Person(a)))//{"x":{"a":0}}
Based on answer.
The problem seems to be the inability of play-json macros to work with nested type variables:
Map[String, Option[Double]]
You could use an intermediate type wrapping Option
import play.api.libs.json.{Json, OFormat}
case class OptionDouble(value: Option[Double])
case class Person(x: Map[String, OptionDouble])
implicit val optionDoubleFormat = Json.format[OptionDouble]
implicit val personFormat = Json.format[Person]
val a: Map[String, OptionDouble] = Map("a" -> OptionDouble(None))
Json.toJson(Person(a))
Another option could be to write the formatter by hand.

How to parse json with arbitrary schema, update/create one field and write it back as json(Scala)

how do one update/create field in JSON object with arbitrary schema and write it back as JSON in Scala?
I tried with spray-json with something like that:
import spray.json._
import DefaultJsonProtocol._
val jsonAst = """{"anyfield":"1234", "sought_optional_field":5.0}""".parse
val newValue = jsonAst.asJsObject.fields.getOrElse("sought_optional_field", 1)
val newMap = jsonAst.asJsObject.fields + ("sought_optional_field" -> newValue)
JSONObject(newMap).toJson
but it gives weird result: "{"anyfield"[ : "1234", "sought_optional_field" : ]1}
You were almost there :
import spray.json._
import DefaultJsonProtocol._
def changeField(json: String) = {
val jsonAst = JsonParser(json)
val map = jsonAst.asJsObject.fields
val sought = map.getOrElse("sought_optional_field", 1.toJson)
map.updated("sought_optional_field", sought).toJson
}
val jsonA = """{"anyfield":"1234", "sought_optional_field":5.0}"""
val jsonB = """{"anyfield":"1234"}"""
changeField(jsonA)
// spray.json.JsValue = {"anyfield":"1234","sought_optional_field":5.0}
changeField(jsonB)
// spray.json.JsValue = {"anyfield":"1234","sought_optional_field":1}
Using Argonaut:
import argonaut._, Argonaut._
def changeField2(json: String) =
json.parseOption.map( parsed =>
parsed.withObject(o =>
o + ("sought_optional_field", o("sought_optional_field").getOrElse(jNumber(1)))
)
)
changeField2(jsonA).map(_.nospaces)
// Option[String] = Some({"anyfield":"1234","sought_optional_field":5})
changeField2(jsonB).map(_.nospaces)
// Option[String] = Some({"anyfield":"1234","sought_optional_field":1})

Play JSON formatter for Map[Int,_]

I am attempting to migrate a Rails/Mongodb application to Play 2.3 using play-reactivemongo and reactivemongo-extensions. In modeling my data I am running across a problem serializing and deserializing a Map[Int,Boolean].
When I try to define my formats via macro like so
implicit val myCaseClass = Json.format[MyCaseClass]
where MyCaseClass has a few string fields, a BSONObjectID field, and a Map[Int,Boolean] field the compiler complains with:
No Json serializer found for type Map[Int,Boolean]. Try to implement an implicit Writes or Format for this type.
No Json deserializer found for type Map[Int,Boolean]. Try to implement an implicit Reads or Format for this type.
Looking at the source code for Play in Reads.scala I see a Reads defined for Map[String,_] but none for Map[Int,_].
Is there a reason why Play has default Read/Writes for string maps but not for other simple types?
I don't fully understand the Map[String,_] defined by play because I am fairly new to scala. How would I go about translating that into a Map[Int,_]? If that is not possible for some technical reason how would I define a Reads/Writes for Map[Int,Boolean]?
you can write your own reads and writes in play.
in your case, this would look like this:
implicit val mapReads: Reads[Map[Int, Boolean]] = new Reads[Map[Int, Boolean]] {
def reads(jv: JsValue): JsResult[Map[Int, Boolean]] =
JsSuccess(jv.as[Map[String, Boolean]].map{case (k, v) =>
Integer.parseInt(k) -> v .asInstanceOf[Boolean]
})
}
implicit val mapWrites: Writes[Map[Int, Boolean]] = new Writes[Map[Int, Boolean]] {
def writes(map: Map[Int, Boolean]): JsValue =
Json.obj(map.map{case (s, o) =>
val ret: (String, JsValueWrapper) = s.toString -> JsBoolean(o)
ret
}.toSeq:_*)
}
implicit val mapFormat: Format[Map[Int, Boolean]] = Format(mapReads, mapWrites)
I have tested it with play 2.3. I'm not sure if it's the best approach to have a Map[Int, Boolean] on server side and a json object with string -> boolean mapping on the client side, though.
JSON only allows string keys (a limitation it inherits from JavaScript).
Play Json provides built-in mapReads and mapWrites for reading and writing Maps.
mapReads takes a (String => JsResult[K]) to let you convert the key to your custom type.
mapWrites returns a Writes[Map[String, Boolean]], and you can use contramap to modify that writer into one that works with a Map[Int, Boolean]
import play.api.libs.json.{JsResult, Reads, Writes}
import scala.util.Try
import play.api.libs.json.Reads.mapReads
import play.api.libs.json.MapWrites.mapWrites
object MapExample {
implicit val reads: Reads[Map[Int, Boolean]] =
mapReads[Int, Boolean](s => JsResult.fromTry(Try(s.toInt)))
implicit val writes: Writes[Map[Int, Boolean]] =
mapWrites[Boolean].contramap(_.map { case (k, v) => k.toString -> v})
}
Thanks to Seth Tisue. This is my "generics" (half) way.
"half" because it does not handle a generic key. one can copy paste and replace the "Long" with "Int"
"Summary" is a type I've wanted to serialize (and it needed its own
serializer)
/** this is how to create reader and writer or format for Maps*/
// implicit val mapReads: Reads[Map[Long, Summary]] = new MapLongReads[Summary]
// implicit val mapWrites: Writes[Map[Long, Summary]] = new MapLongWrites[Summary]
implicit val mapLongSummaryFormat: Format[Map[Long, Summary]] = new MapLongFormats[Summary]
This is the required implementation:
class MapLongReads[T]()(implicit reads: Reads[T]) extends Reads[Map[Long, T]] {
def reads(jv: JsValue): JsResult[Map[Long, T]] =
JsSuccess(jv.as[Map[String, T]].map{case (k, v) =>
k.toString.toLong -> v .asInstanceOf[T]
})
}
class MapLongWrites[T]()(implicit writes: Writes[T]) extends Writes[Map[Long, T]] {
def writes(map: Map[Long, T]): JsValue =
Json.obj(map.map{case (s, o) =>
val ret: (String, JsValueWrapper) = s.toString -> Json.toJson(o)
ret
}.toSeq:_*)
}
class MapLongFormats[T]()(implicit format: Format[T]) extends Format[Map[Long, T]]{
override def reads(json: JsValue): JsResult[Map[Long, T]] = new MapLongReads[T].reads(json)
override def writes(o: Map[Long, T]): JsValue = new MapLongWrites[T].writes(o)
}
We can generalize the solution of 3x14159265 and Seth Tisue thanks to 2 small type classes:
import play.api.libs.json.Json.JsValueWrapper
import play.api.libs.json._
import simulacrum._
object MapFormat {
#typeclass trait ToString[A] {
def toStringValue(v: A): String
}
#typeclass trait FromString[A] {
def fromString(v: String): A
}
implicit final def mapReads[K: FromString, V: Reads]: Reads[Map[K, V]] =
new Reads[Map[K, V]] {
def reads(js: JsValue): JsResult[Map[K, V]] =
JsSuccess(js.as[Map[String, V]].map { case (k, v) => FromString[K].fromString(k) -> v })
}
implicit final def mapWrites[K: ToString, V: Writes]: Writes[Map[K, V]] =
new Writes[Map[K, V]] {
def writes(map: Map[K, V]): JsValue =
Json.obj(map.map {
case (s, o) =>
val ret: (String, JsValueWrapper) = ToString[K].toStringValue(s) -> o
ret
}.toSeq: _*)
}
implicit final def mapFormat[K: ToString: FromString, V: Format]: Format[Map[K, V]] = Format(mapReads, mapWrites)
}
Note that I use Simulacrum (https://github.com/mpilquist/simulacrum) to define my type classes.
Here is an example of how to use it:
final case class UserId(value: String) extends AnyVal
object UserId {
import MapFormat._
implicit final val userToString: ToString[UserId] =
new ToString[UserId] {
def toStringValue(v: UserId): String = v.value
}
implicit final val userFromString: FromString[UserId] =
new FromString[UserId] {
def fromString(v: String): UserId = UserId(v)
}
}
object MyApp extends App {
import MapFormat._
val myMap: Map[UserId, Something] = Map(...)
Json.toJson(myMap)
}
if IntelliJ says that your import MapFormat._ is never used, you can and this: implicitly[Format[Map[UserId, Something]]] just below the import. It'll fix the pb. ;)
A specific KeyWrites and KeyReads is available in play-json 2.9.x
private implicit val longKeyWrites = KeyWrites[Int](_.toString)
private implicit val longKeyReads =
KeyReads[Int](str => Try(str.toInt).fold(e => JsError(e.getMessage), JsSuccess(_)))
Json.obj("1" -> "test").validate[Map[Int,String]] // JsSuccess(Map(1 -> test))
Like the accepted answer - a bit shorter:
implicit val mapReads: Reads[Map[Int, Boolean]] = (jv: JsValue) =>
JsSuccess(jv.as[Map[String, Boolean]].map { case (k, v) =>
k.toInt -> v
})
implicit val mapWrites: Writes[Map[Int, Boolean]] = (map: Map[Int, Boolean]) =>
Json.toJson(map.map { case (s, o) =>
s.toString -> o
})
implicit val jsonMapFormat: Format[Map[Int, Boolean]] = Format(mapReads, mapWrites)
Here a little test:
val json = Json.toJson(Map(1 -> true, 2 -> false))
println(json) // {"1":true,"2":false}
println(json.validate[Map[Int, Boolean]]) // JsSuccess(Map(1 -> true, 2 -> false),)
https://gist.github.com/fancellu/0bea53f1a1dda712e179892785572ce3
Here is a way to persist a Map[NotString,...]

Deserializer for List[T] types

I'm trying to deserialize a JsArray into a List[T] in a playframework application using Scala. After some research I found this method which is supposed to do the needed work:
/**
* Deserializer for List[T] types.
*/
implicit def listReads[T](implicit fmt: Reads[T]): Reads[List[T]] = new Reads[List[T]] {
def reads(json: JsValue) = json match {
case JsArray(ts) => ts.map(t => fromJson(t)(fmt)).toList
case _ => throw new RuntimeException("List expected")
}
}
The problem is that I didn't know how to use it. Any help is welcome.
Here's a quick example:
scala> import play.api.libs.json._
import play.api.libs.json._
scala> Json.toJson(List(1, 2, 3)).as[List[Int]]
res0: List[Int] = List(1, 2, 3)
And if you have a custom type with a Format instance:
case class Foo(i: Int, x: String)
implicit object fooFormat extends Format[Foo] {
def reads(json: JsValue) = Foo(
(json \ "i").as[Int],
(json \ "x").as[String]
)
def writes(foo: Foo) = JsObject(Seq(
"i" -> JsNumber(foo.i),
"x" -> JsString(foo.x)
))
}
It still works:
scala> val foos = Foo(1, "a") :: Foo(2, "bb") :: Nil
foos: List[Foo] = List(Foo(1,a), Foo(2,bb))
scala> val json = Json.toJson(foos)
json: play.api.libs.json.JsValue = [{"i":1,"x":"a"},{"i":2,"x":"bb"}]
scala> json.as[List[Foo]]
res1: List[Foo] = List(Foo(1,a), Foo(2,bb))
This approach would also work if your custom type had a xs: List[String] member, for example: you'd just use (json \ "xs").as[List[String]] in your reads method and Json.toJson(foo.xs) in your writes.