Play Json: custom reads one field - json

Let's say I have to write custom Reads[Person] for Person class:
import play.api.libs.functional.syntax._
implicit val personReads: Reads[Person] = (
(__ \ "name").read[String] and // or ~
(__ \ "age").readNullable[Int]
) ((name, age) => Person(name = name, age = age))
it works like a charm, really (no).
But what can I do when there is only one field in json object?
The core of Reads and Writes is in functional syntax which transforms these "parse" steps.
The following does not compile:
import play.api.libs.functional.syntax._
implicit val personReads: Reads[Person] = (
(__ \ "name").read[String]
)(name => Person(name))
Could you advice how to deal with it?

Option 1: Reads.map
import play.api.libs.json._
case class Person(name: String)
object PlayJson extends App {
implicit val readsPeson: Reads[Person] =
(__ \ "name").read[String].map(name => Person(name))
val rawString = """{"name": "John"}"""
val json = Json.parse(rawString)
val person = json.as[Person]
println(person)
}
Option 2: Json.reads
import play.api.libs.json._
case class Person(name: String)
object Person {
implicit val readsPerson = Json.reads[Person]
}
object PlayJson extends App {
val rawString = """{"name": "John"}"""
val json = Json.parse(rawString)
val person = json.as[Person]
println(person)
}

Related

Scala - How to implement implicit JSON reader

I've implemented implicit Json Reads in order to read two fields from JSON, saleId and saleType. I wanted to make getSales method return a tuple (Int, String) representing saleId and saleType accordingly. But when I call the getSales method I'm getting the following errors:
Error:(46, 79) No JSON deserializer found for type (Int, String). Try to implement an implicit Reader or JsonFormat for this type.
val salesData = (salesJson \ "sales").as[(Int, String)]
Error:(46, 79) not enough arguments for method as: (implicit reader: org.json4s.Reader[(Int, String)], implicit mf: Manifest[(Int, String)])(Int, String).
Unspecified value parameters reader, mf.
val salesData = (salesJson \ "sales").as[(Int, String)]
I have implemented implicit Json Reads so really confused with the first error. Here is my implementation:
def getsales(context: SparkContext, saleId: Int): (Int, String)= {
val url= buildUrl(context, saleId)
implicit val salesReader: Reads[(Int, String)] = (
(__ \ "id_from_API").read[Int] and
(__ \ "sale_type").read[String]
).tupled
val salesJson: JValue = parse(httpStringResponse(url, context))
val salesData = (salesJson \ "sales_stats").as[(Int, String)]
salesData
}
Two notes concerning you code:
val salesData = (salesJson \ "sales").as[(Int, String)]
val salesData = (salesJson \ "sales_stats").as[(Int, String)]
might have to be the same.
Instead of JValue you might have wanted to put JsValue in the line
val salesJson: JValue = parse(httpStringResponse(url, context))
Other than that testing your JSON reader code separately from the rest of your code might be helpful.
The following worked for me:
import org.scalatest.WordSpec
import play.api.libs.functional.syntax._
import play.api.libs.json._
class ReadsExample extends WordSpec {
"read" in {
val sales =
"""
{
"sales_stats": {
"id_from_API": 42,
"sale_type": "cheap"
}
}
""".stripMargin
implicit val salesReader: Reads[(Int, String)] = (
(JsPath \ "id_from_API").read[Int] and
(JsPath \ "sale_type").read[String]
).tupled
val salesJson: JsValue = Json.parse(sales)
val salesData = (salesJson \ "sales_stats").as[(Int, String)]
}
}
Please note that the version of play-json used here is 2.3.10.
EDIT
code example to the question in the comment
import org.scalatest.WordSpec
import play.api.libs.json.Json.reads
import play.api.libs.json.{Json, _}
class ReadsExample extends WordSpec {
"read" in {
val sales =
"""
{
"id_from_API": 9,
"sale_type": {
"main" : "a",
"sub" : "b"
}
}
""".stripMargin
val salesJson: JsValue = Json.parse(sales)
val salesData = salesJson.as[Sales]
}
}
case class Sales(id_from_API: Int, sale_type: SaleType)
case class SaleType(main: String, sub: String)
object Sales {
implicit val st: Reads[SaleType] = reads[SaleType]
implicit val of: Reads[Sales] = reads[Sales]
}

Scala Graph JSON with Play Framework

I am trying to pass in a POST request to a REST API developped with Play! (2.5) an object that I would like to use a Scala Graph (from the graph-core dependency).
It looks like the graph already has JSON serialization/deserialization methods based on lift-json, but I am not sure how to "plug" that into Play Json library. Until now I was using implicit converters (with Reads/Writes methods) but I would like to avoid having to write my own methods for the graph part since it is already part of the library itself.
For instance, let's say I have this code:
import java.util.UUID
import scalax.collection.Graph
case class Task(
id: UUID,
status: String)
case class Stuff(
id: UUID = UUID.randomUUID(),
name: String,
tasks: Option[Graph[Task, DiEdge]])
implicit val stuffWrites: Writes[Stuff] = (
(JsPath \ "id").write[UUID] and
(JsPath \ "name").write[String]
)(unlift(Stuff.unapply))
implicit val stuffReads: Reads[Stuff] = (
(JsPath \ "id").read[UUID] and
(JsPath \ "name").read[String]
)(Stuff.apply _)
implicit val taskWrite: Writes[Task] = (
(JsPath \ "id").write[UUID] and
(JsPath \ "status").write[String]
)(unlift(Task.unapply))
implicit val taskReads: Reads[Task] = (
(JsPath \ "id").read[UUID] and
(JsPath \ "status").read[String]
)(Task.apply _)
I miss the part to serialize the graph and the parenting. Should I rewrite everything from scratch, or can I rely on methods toJson/fromJson from scalax.collection.io.json ?
Since I struggled a bit to get this working, I thought I would share the code:
class UUIDSerializer extends Serializer[UUID] {
private val UUIDClass = classOf[UUID]
def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), UUID] = {
case (TypeInfo(UUIDClass, _), json) => json match {
case JString(id) => UUID.fromString(id)
case x => throw new MappingException("Can't convert " + x + " to UUID")
}
}
def serialize(implicit format: Formats): PartialFunction[Any, JValue] = {
case x: UUID => JString(x.toString)
}
}
val extraSerializers = new UUIDSerializer :: Nil
implicit val formats = Serialization.formats(NoTypeHints) ++ extraSerializers
val taskDescriptor = new NodeDescriptor[Task](typeId = "Tasks", customSerializers=extraSerializers) {
def id(node: Any) = node match {
case Task(id, _) => id.toString
}
}
val quickJson = new Descriptor[Task](
defaultNodeDescriptor = taskDescriptor,
defaultEdgeDescriptor = Di.descriptor[Task]()
)
implicit val tasksWrites = new Writes[Graph[Task, DiEdge]] {
def writes(graph: Graph[Task, DiEdge]): JsValue = {
val json = graph.toJson(quickJson)
Json.parse(json.toString)
}
}
implicit val tasksReads = new Reads[Graph[Task, DiEdge]] {
def reads(json: JsValue): JsResult[Graph[Task, DiEdge]] = {
try {
val graph = Graph.fromJson[Task, DiEdge](json.toString, quickJson)
JsSuccess(graph)
}
catch {
case e: Exception =>
JsError(e.toString)
}
}
}
implicit def stuffModelFormat = Jsonx.formatCaseClass[Stuff]
You can try writing companion objects for yours case classes where you specify the formatting.
Example:
object Task {
implicit val taskModelFormat = Json.format[Task]
}
object Stuff {
implicit val staffModelFormat = Json.format[Stuff]
}
instead of the above implicits. With this solution compiler will resolve the known formatters for you and you could be only required to specify the missing/unknown types instead of the whole structure.

How to define a `Write` for an case class which will generate nested custom fields?

Play framework provided some DSL to read and write JSON, e.g.
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class User(name:String, age:Option[Int])
implicit val userWrites = (
(__ \ "name" ).write[String] and
(__ \ "age" ).writeNullable[Int]
)(unlift(User.unapply))
val user= new User("Freewind", Some(100))
Json.toJson(user)
It will generate a json:
{"name":"Freewind","age":100}
But how to define the userWrites to generate such a JSON:
{
"name" : "Freewind",
"age" : 100,
"nested" : {
"myname" : "Freewind",
"myage" : 100
}
}
I tried some solutions but none can work.
You can achieve that with JSON transformers, like in this code:
object Test {
def main(args: Array[String]): Unit = {
import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._
case class User(name: String, age: Option[Int])
implicit val userWrites = (
(__ \ "name").write[String] and
(__ \ "age").writeNullable[Int])(unlift(User.unapply))
val jsonTransformer = (__).json.update(
__.read[JsObject].map { o => o ++ Json.obj("nested" -> Json.obj()) }) and
(__ \ 'nested).json.copyFrom((__).json.pick) reduce
val user = new User("Freewind", Some(100))
val originalJson = Json.toJson(user)
println("Original: " + originalJson)
val transformedJson = originalJson.transform(jsonTransformer).get
println("Tansformed: " + transformedJson)
}
}
The output would be this:
Original: {"name":"Freewind","age":100}
Tansformed: {"name":"Freewind","age":100,"nested":{"name":"Freewind","age":100}}

Play: How to transform JSON while writing/reading it to/from MongoDB

Here is an simple JSON I want to write/read to/from MongoDB:
{
"id": "ff59ab34cc59ff59ab34cc59",
"name": "Joe",
"surname": "Cocker"
}
Before storing it in MongoDB, "ff59ab34cc59ff59ab34cc59" has to be transformed into an ObjectID and id renamed to _id... so given the following Reads, how do I achieve that?
val personReads: Reads[JsObject] = (
(__ \ 'id).read[String] ~ // how do I rename id to _id AND transform "ff59ab34cc59ff59ab34cc59" to an ObjectID?
(__ \ 'name).read[String] ~
(__ \ 'surname).read[String]
) reduce
And of course, I also need the contrary for my Writes, i.e. renaming _id to id and transforming an ObjectID to plain text in the format "ff59ab34cc59ff59ab34cc59".
JsonExtensions
I usually have a JsExtensions object in my application which looks like the following :
import reactivemongo.bson.BSONObjectID
object JsonExtensions {
import play.api.libs.json._
def withDefault[A](key: String, default: A)(implicit writes: Writes[A]) = __.json.update((__ \ key).json.copyFrom((__ \ key).json.pick orElse Reads.pure(Json.toJson(default))))
def copyKey(fromPath: JsPath,toPath:JsPath ) = __.json.update(toPath.json.copyFrom(fromPath.json.pick))
def copyOptKey(fromPath: JsPath,toPath:JsPath ) = __.json.update(toPath.json.copyFrom(fromPath.json.pick orElse Reads.pure(JsNull)))
def moveKey(fromPath:JsPath, toPath:JsPath) =(json:JsValue)=> json.transform(copyKey(fromPath,toPath) andThen fromPath.json.prune).get
}
For a simple model
case class SOUser(name:String,_id:BSONObjectID)
you can write your json serializer/deserializer like this:
object SOUser{
import play.api.libs.json.Format
import play.api.libs.json.Json
import play.modules.reactivemongo.json.BSONFormats._
implicit val soUserFormat= new Format[SOUser]{
import play.api.libs.json.{JsPath, JsResult, JsValue}
import JsonExtensions._
val base = Json.format[SOUser]
private val publicIdPath: JsPath = JsPath \ 'id
private val privateIdPath: JsPath = JsPath \ '_id \ '$oid
def reads(json: JsValue): JsResult[SOUser] = base.compose(copyKey(publicIdPath, privateIdPath)).reads(json)
def writes(o: SOUser): JsValue = base.transform(moveKey(privateIdPath,publicIdPath)).writes(o)
}
}
here is what you get in the console :
scala> import reactivemongo.bson.BSONObjectID
import reactivemongo.bson.BSONObjectID
scala> import models.SOUser
import models.SOUser
scala> import play.api.libs.json.Json
import play.api.libs.json.Json
scala>
scala> val user = SOUser("John Smith", BSONObjectID.generate)
user: models.SOUser = SOUser(John Smith,BSONObjectID("52d00fd5c912c061007a28d1"))
scala> val jsonUser=Json.toJson(user)
jsonUser: play.api.libs.json.JsValue = {"name":"John Smith","id":"52d00fd5c912c061007a28d1","_id":{}}
scala> Json.prettyPrint(jsonUser)
res0: String =
{
"name" : "John Smith",
"id" : "52d00fd5c912c061007a28d1",
"_id" : { }
}
scala> jsonUser.validate[SOUser]
res1: play.api.libs.json.JsResult[models.SOUser] = JsSuccess(SOUser(John Smith,BSONObjectID("52d00fd5c912c061007a28d1")),/id)
Applying this to your example
val _personReads: Reads[JsObject] = (
(__ \ 'id).read[String] ~
(__ \ 'name).read[String] ~
(__ \ 'surname).read[String]
).reduce
Doesn't compile by default, I guess you meant to write :
val _personReads: Reads[(String,String,String)] = (
(__ \ 'id).read[String] ~
(__ \ 'name).read[String] ~
(__ \ 'surname).read[String]
).tupled
in which case you can do the following
import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._
import play.modules.reactivemongo.json.BSONFormats._
import reactivemongo.bson.BSONObjectID
def copyKey(fromPath: JsPath,toPath:JsPath ) = __.json.update(toPath.json.copyFrom(fromPath.json.pick))
val json = """{
"id": "ff59ab34cc59ff59ab34cc59",
"name": "Joe",
"surname": "Cocker"
}"""
val originaljson = Json.parse(json)
val publicIdPath: JsPath = JsPath \ 'id
val privateIdPath: JsPath = JsPath \ '_id \ '$oid
val _personReads: Reads[(BSONObjectID,String,String)] = (
(__ \ '_id).read[BSONObjectID] ~
(__ \ 'name).read[String] ~
(__ \ 'surname).read[String]
).tupled
val personReads=_personReads.compose(copyKey(publicIdPath,privateIdPath))
originaljson.validate(personReads)
// yields res5: play.api.libs.json.JsResult[(reactivemongo.bson.BSONObjectID, String, String)] = JsSuccess((BSONObjectID("ff59ab34cc59ff59ab34cc59"),Joe,Cocker),/id)
or you meant that you want to move the value of the id key to _id \ $oid which can be accomplished with
import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._
import play.modules.reactivemongo.json.BSONFormats._
import reactivemongo.bson.BSONObjectID
def copyKey(fromPath: JsPath,toPath:JsPath ) = __.json.update(toPath.json.copyFrom(fromPath.json.pick))
val json = """{
"id": "ff59ab34cc59ff59ab34cc59",
"name": "Joe",
"surname": "Cocker"
}"""
val originaljson = Json.parse(json)
val publicIdPath: JsPath = JsPath \ 'id
val privateIdPath: JsPath = JsPath \ '_id \ '$oid
originaljson.transform(copyKey(publicIdPath,privateIdPath) andThen publicIdPath.json.prune)
You can't have a BSONObjectID in there for now since you are manipulating object from the JsValue type hierarchy. When you pass json to reactivemongo it is converted to a BSONValue. A JsObject will be converted to a BSONDocument. if the JsObject contains a path for _id\$oid this path will be converted to a BSONObjectId automatically and it will be stored as an ObjectID in mongodb.
The original question is really about reactivemongo's (sgodbillon et al) treatment of the native mongodb _id. The chosen answer is instructive and correct but obliquely addresses the OP's concern whether "it will all just work".
Thanks to https://github.com/ReactiveMongo/ReactiveMongo-Play-Json/blob/e67e507ecf2be48cc71e429919f7642ea421642c/src/main/scala/package.scala#L241-L255, I believe it will.
import scala.concurrent.Await
import scala.concurrent.duration.Duration
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.functional.syntax._
import play.api.libs.json._
import play.modules.reactivemongo.json.collection.JSONCollection
import reactivemongo.api._
import reactivemongo.bson.BSONObjectID
import reactivemongo.play.json._
case class Person(
id: BSONObjectID,
name: String,
surname: String
)
implicit val PersonFormat: OFormat[Person] = (
(__ \ "_id").format[BSONObjectID] and
(__ \ "name").format[String] and
(__ \ "surname").format[String]
)(Person.apply, unlift(Person.unapply))
val driver = new reactivemongo.api.MongoDriver
val connection = driver.connection(List("localhost"))
val db = connection.db("test")
val coll = db.collection[JSONCollection]("persons")
coll.drop(false)
val id = BSONObjectID.generate()
Await.ready(coll.insert(Person(id, "Joe", "Cocker")), Duration.Inf)
Await.ready(coll.find(Json.obj()).one[Person] map { op => assert(op.get.id == id, {}) }, Duration.Inf)
The above is a minimal working example of your case class using id and the database storing it as _id. Both are instantiated as 12-byte BSONObjectIDs.

Create implicit json read for List collection which might be missing from input json

I am following play-salat (github.com/leon/play-salat) to create a model for a json input and save to mongodb. How can I create the implicit json read for List collection which might be missing in the input json? The following code gives me the validation error if the 'positions' is missing from input json.
case class LIProfile(
id: ObjectId = new ObjectId,
positions: List[Position] = Nil
)
object LIProfile extends LIProfileDAO with LIProfileJson
trait LIProfileDAO extends ModelCompanion[LIProfile, ObjectId] {
def collection = mongoCollection("liprofiles")
val dao = new SalatDAO[LIProfile, ObjectId](collection) {}
// Indexes
collection.ensureIndex(DBObject("emailAddress" -> 1), "li_profile_email", unique = true)
// Queries
def findOneByEmail(email: String): Option[LIProfile] = dao.findOne(MongoDBObject("emailAddress" -> email))
}
trait LIProfileJson {
implicit val liprofileJsonWrite = new Writes[LIProfile] {
def writes(p: LIProfile): JsValue = {
Json.obj(
"id" -> p.id,
"positions" -> p.positions
)
}
}
implicit val liprofileJsonRead = (
(__ \ 'id).read[ObjectId] ~
(__ \ 'positions).read (
(__ \ 'values).read[List[Position]]
)
)(LIProfile.apply _)
}
Use readNullable in order to retrieve an Option and map that to the contained list or the empty list.
implicit val liprofileJsonRead = (
(__ \ 'id).read[ObjectId] ~
(__ \ 'positions).readNullable (
(__ \ 'values).read[List[Position]]
).map {
case Some(l) => l
case None => Nil
}
)(LIProfile)
or even shorter:
implicit val liprofileJsonRead = (
(__ \ 'id).read[ObjectId] ~
(__ \ 'positions).readNullable (
(__ \ 'values).read[List[Position]]
).map { l => l.getOrElse(Nil) }
)(LIProfile)
I'm not quite sure what imports you really need here, my code compiles using:
import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._