Play JSON combinator validates at least a field is specified - json

I have a scenario that when parsing a json to a case class like this
implicit val userRead: Reads[User] = (
(__ \ "name").read[String] ~
(__ \ "email").readNullable[String] ~
(__ \ "phone").readNullable[String] ~
Reads.pure(None)
)(User.apply _)
I don't require both email and phone to be available, but at least one of them have to be available.
In my case class definition, I can prevent the case that both of them are empty with
case class User(name: String, email: Option[String], phone: Option[String], id: Option[Long] = None) {
require(email.nonEmpty || phone.nonEmpty, "must have at least an email or phone number")
}
However, doing this will generate an exception, and a 500 status is responded, when this should be a 400 error due to user input.
I can of course manually perform the verification in my controller, but I wonder if there is a cleaner way to do this.

I can only suggest writing you own "reads" manually
case class A(i: Option[Int], j: Option[Int])
implicit val reads: Reads[A] = new Reads[A] {
override def reads(json: JsValue): JsResult[A] = {
(for {
i <- (json \ "i").validateOpt[Int]
j <- (json \ "j").validateOpt[Int]
} yield A(i, j))
.filter(JsError("both values can't be empty"))(a ⇒ a.j.nonEmpty || a.i.nonEmpty)
}
}
And to test:
scala> Json.parse("""{ "i" : 1} """).validate[A]
res4: play.api.libs.json.JsResult[A] = JsSuccess(A(Some(1),None),)
scala> Json.parse("""{} """).validate[A]
res5: play.api.libs.json.JsResult[A] = JsError(List((,List(ValidationError(List(from and to in range can't be both empty),WrappedArray())))))

You can achieve this with just a small addition to your existing code (.filter(ValidationError("must have at least an email or phone number"))(u => u.email.isDefined || u.phone.isDefined)):
case class User(name: String, email: Option[String], phone: Option[String], id: Option[Long] = None)
implicit val userRead: Reads[User] = (
(__ \ "name").read[String] ~
(__ \ "email").readNullable[String] ~
(__ \ "phone").readNullable[String] ~
Reads.pure(None)
)(User.apply _).filter(ValidationError("must have at least an email or phone number"))(u => u.email.isDefined || u.phone.isDefined)
Now, to show that this works:
scala> Json.parse("""{ "name" : "foobar"} """).validate[User]
res3: play.api.libs.json.JsResult[User] = JsError(List((,List(ValidationError(List(must have at least an email or phone number),WrappedArray())))))
scala> Json.parse("""{ "name" : "foobar", "email":"test"} """).validate[User]
res4: play.api.libs.json.JsResult[User] = JsSuccess(User(foobar,Some(test),None,None),)
scala> Json.parse("""{ "name" : "foobar", "phone":"test"} """).validate[User]
res5: play.api.libs.json.JsResult[User] = JsSuccess(User(foobar,None,Some(test),None),)

Related

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/Scala how to prevent Json serialization of empty arrays?

I want to recursively write a class to Json, so I'm using the following implicit writes:
implicit val writesObject : Writes[Object] = (
(__ \ "id").writeNullable[String] ~
(__ \ "list").lazyWriteNullable(Writes.traversableWrites[Object](writesObject))
)(unlift(Object.unapply)
where Object is a class like this:
case class Object(id: Option[String], list: Option[Seq[Object]])
It works, however I would like to prevent it from printing anything if "list" is empty. For example:
I want:
{ id: "someID",
list: [
{
id: "someOtherId"
}
]
}
I currently get(but don't want):
{ id: "someID",
list: [
{
id: "someOtherId"
list: []
}
]
}
How can I achieve this? I'm new to Play/Scala and not sure exactly what should I be looking at so any pointers would be helpful. I'm using Scala 2.2.1.
PS: I've checked Scala Json Combinators but didn't see any reference on how to get this done.
Update:
So my issue is not that list is null, but that list is empty. That's why lazyWriteNullable wasn't working.
Testing johanandren answer I came up with the following extension to JsPath that returns Option[T] and supports the lazy format for recursive writes:
def lazyWriteNullableIterable[T <: Iterable[_]](w: => Writes[T]): OWrites[Option[T]] = OWrites((t: Option[T]) => {
if(t != null) {
t.getOrElse(Seq.empty).size match {
case 0 => Json.obj()
case _ => Writes.nullable[T](path)(w).writes(t)
}
}
else {
Json.obj()
}
})
Thanks
You can create a custom OFormat that will do this. By implicitly decorating JsPath with it you can include it in your json combinator definitions:
implicit class PathAdditions(path: JsPath) {
def readNullableIterable[A <: Iterable[_]](implicit reads: Reads[A]): Reads[A] =
Reads((json: JsValue) => path.applyTillLast(json).fold(
error => error,
result => result.fold(
invalid = (_) => reads.reads(JsArray()),
valid = {
case JsNull => reads.reads(JsArray())
case js => reads.reads(js).repath(path)
})
))
def writeNullableIterable[A <: Iterable[_]](implicit writes: Writes[A]): OWrites[A] =
OWrites[A]{ (a: A) =>
if (a.isEmpty) Json.obj()
else JsPath.createObj(path -> writes.writes(a))
}
/** When writing it ignores the property when the collection is empty,
* when reading undefined and empty jsarray becomes an empty collection */
def formatNullableIterable[A <: Iterable[_]](implicit format: Format[A]): OFormat[A] =
OFormat[A](r = readNullableIterable(format), w = writeNullableIterable(format))
}
This would allow you to create formats/reads/writes using the json combinator syntax like this:
case class Something(as: List[String], v: String)
import somewhere.PathAdditions
val reads: Reads[Something] = (
(__ \ "possiblyMissing").readNullableIterable[List[String]] and
(__ \ "somethingElse").read[String]
)(Something)
val writes: Writes[Something] = (
(__ \ "possiblyMissing").writeNullableIterable[List[String]] and
(__ \ "somethingElse").write[String]
)(unlift(Something.unapply))
val format: Format[Something] = (
(__ \ "possiblyMissing").formatNullableIterable[List[String]] and
(__ \ "somethingElse").format[String]
)(Something, unlift(Something.unapply))

Play: How to modify data while deserializing Json

Here below is a simple case class that holds user information:
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class User(
val username: String,
val password: String
)
... and here is the companion object that provides functionality for serializing/deserializing User objects to/from JSON:
object User {
implicit val userWrites: Writes[User] = (
(__ \ 'username).write[String] ~
(__ \ 'password).write[String]
)(unlift(User.unapply))
implict val userReads: Reads[User] = (
(__ \ 'username).read[String] ~
(__ \ 'password).read[String] // how do I invoke the password hasher here?
)(User.apply(_, _))
}
Let's assume User objects should always contain hashed passwords but incoming JSON always provide plaintext... how can I enhance my Reads so that it invokes the password hasher while deserializing?
You can use the map method on Reads:
def doSomething(s: String) = s * 3
case class User(val username: String, val password: String)
object User {
implicit val userWrites: Writes[User] = (
(__ \ 'username).write[String] ~
(__ \ 'password).write[String]
)(unlift(User.unapply))
implicit val userReads: Reads[User] = (
(__ \ 'username).read[String] ~
(__ \ 'password).read[String].map(doSomething)
)(User.apply(_, _))
}
And then:
scala> Json.parse("""{"username": "foo", "password": "bar"}""").as[User]
res0: User = User(foo,barbarbar)
Note, though, that this means that your Reads and Writes are no longer inverses of each other.

Play 2.2 JSON Reads with combinators: how to deal with nested optional objects?

I'm going crazy trying to parse this JSON structure in Play Framework 2.2:
val jsonStr = """{ personFirstName: "FirstName",
personLastName: "LastName"
positionLat: null,
positionLon: null }"""
I have 2 case classes:
case class Position( val lat: Double, val lon: Double)
case class Person( firstName: String, lastName: String, p: Option[Position] )
As you can see, Position is not mandatory in Person case class.
I was trying to get an instance of Person using something like this
implicit val reader = (
(__ \ 'personFirstName ).read[String] ~
(__ \ 'personLastName ).read[String] ~
( (__ \ 'positionLat ).read[Double] ~
(__ \ 'positionLon ).read[Double] )(Position)
)(Person)
but I soon realized I have no idea how to deal with the Option[Position] object: the intention would be to instantiate a Some(Position(lat,lon)) if both 'lat' and 'lon' are specified and not null, otherwise instantiate None.
How would you deal with that?
I'm pretty sure there's a better way of doing what you want than what I'm going to post, but it's late and I can't figure it out now. I'm assuming that simply changing the JSON structure you're consuming isn't an option here.
You can supply a builder function that takes two optional doubles for lat/lon and yields a position if they're both present.
import play.api.libs.functional.syntax._
import play.api.libs.json._
val jsonStr = """{
"personFirstName": "FirstName",
"personLastName": "LastName",
"positionLat": null,
"positionLon": null }"""
case class Position(lat: Double, lon: Double)
case class Person( firstName: String, lastName: String, p: Option[Position] )
object Person {
implicit val reader = (
(__ \ "personFirstName" ).read[String] and
(__ \ "personLastName" ).read[String] and (
(__ \ "positionLat" ).readNullable[Double] and
(__ \ "positionLon" ).readNullable[Double]
)((latOpt: Option[Double], lonOpt: Option[Double]) => {
for { lat <- latOpt ; lon <- lonOpt} yield Position(lat, lon)
})
)(Person.apply _)
}
Json.parse(jsonStr).validate[Person] // yields JsSuccess(Person(FirstName,LastName,None),)
Also, note that to be valid JSON you need to quote the data keys.
Your javascript object should match the structure of your case classes. Position will need to have a json reader as well.
val jsonStr = """{ "personFirstName": "FirstName",
"personLastName": "LastName",
"position":{
"lat": null,
"lon": null
}
}"""
case class Person( firstName: String, lastName: String, p: Option[Position] )
object Person {
implicit val reader = (
(__ \ 'personFirstName ).read[String] ~
(__ \ 'personLastName ).read[String] ~
(__ \ 'position ).readNullable[Position]
)(Person.apply _)
}
case class Position( val lat: Double, val lon: Double)
object Position {
implicit val reader = (
(__ \ 'lat ).read[Double] ~
(__ \ 'lon ).read[Double]
)(Position.apply _)
}
If either of the fields of Position are null/missing in the json object, it will be parsed as None. So, jsonStr.as[Person] = Person("FirstName", "LastName", None)

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._