Play Framework: How to Validate Optional Fields When Parsing JSON - json

Given the following JSON...
{
"nickname": "mj",
"firstname": "Mike",
"lastName": "Jordan",
"trash": "ignore"
}
... I need to parse and validate it so that
any field except nickname, firstName, and lastName are filtered out – in my example above trash has to be removed
if defined, nickname (which is optional) must be at least 3 characters long
Here below is my code:
import play.api.libs.json._
import play.api.libs.functional.syntax._
def orEmpty = reads | __.json.put(Json.obj())
val js = Json.parse("""{ "nickname": "mj", "firstname": "Mike", "lastName": "Jordan" }""")
val validateUser = (
((__ \ 'nickname).json.pickBranch(Reads.of[JsString] <~ Reads.minLength[String](3)) orEmpty) ~
((__ \ 'firstName).json.pickBranch) ~
((__ \ 'lastName).json.pickBranch)
)
validateUser.reads(js).fold(
valid = { validated => JsSuccess(js) },
invalid => { errors => JsError(errors) }
)
The problem is that if nickname is invalid because shorter than 3 characters, orEmpty applies and no error is reported. What I need is to keep nickname optional (that's why I defined orEmpty), but when defined the validation should succeed if and only if nickanme passes the Reads.minLength check.

Assuming Play Framework 2.4.x. I believe reading nullable fields is a little different in 2.3.x but it's the same idea.
It would be easier to reason about this if you assume that JSON validators and transformers are really just Reads[T <: JsValue].
Overall, what you need to consider is that the nickname field is actually optional and you need to represent it as such. Basically, what you're looking for is to compose the nullableReads with Reads.minLength to read nickname as Option[String].
So, you can represent the structure you want to validate as:
case class ToValidate(nickname: Option[String], firstname: String, lastname: String)
And define you're Reader:
import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._
implicit val toValidateReads: Reads[ToValidate] = (
(__ \ "nickname").readNullable[String](minLength(3)) ~
(__ \ "firstname").read[String] ~
(__ \ "lastname").read[String]
)(ToValidate.apply _)
Then you can vaildate your input:
val js = Json.parse("""{ "nickname": "mj", "firstname": "Mike", "lastname": "Jordan" }""")
val v = js.validate[ToValidate]
println(v) // JsError(List((/nickname,List(ValidationError(List(error.minLength),WrappedArray(3))))))

Related

PlayJson Scala Read Nullable Fields

I have a contact class, it reads (JsPath \ "contact_name" \ "first_name" ) to firstName, but (JsPath \ "contact_name") can be empty. Does anyone know how to do the Reader for this case class?
case class Contact(var firstName: Option[String],
var lastName: Option[String])
And my contact Json is:
{
"contact_name": {
"first_name": "hello",
"last_name": "world"
},
"phone_number": "1231231234",
"email": "test#gmail.com"
}
Contact Json without "contact_name":
{
"phone_number": "1231231234",
"email": "test#gmail.com"
}
I want Both Json to be able to read to Contact objects. Thank you.
Assuming your phone number and email are part of the contact details, here's one that works (you can use \\ to search for paths in depth):
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class Contact(firstName: Option[String], lastName: Option[String],
phone: String, email: String)
val contactReads: Reads[Contact] = (
(__ \\ "first_name").readNullable[String] and
(__ \\ "last_name").readNullable[String] and
(__ \ "phone_number").read[String] and
(__ \ "email").read[String]
)(Contact.apply _)
val json1 = """{
| "contact_name": {
| "first_name": "hello",
| "last_name": "world"
| },
| "phone_number": "1231231234",
| "email": "test#gmail.com"
|}""".stripMargin
Json.parse(json1).validate[Contact](contactReads)
// JsSuccess(Contact(Some(hello),Some(world),1231231234,test#gmail.com),)
val json2 = """{
| "phone_number": "1231231234",
| "email": "test#gmail.com"
|}""".stripMargin
Json.parse(json2).validate[Contact](contactReads)
// JsSuccess(Contact(None,None,1231231234,test#gmail.com),)
I did write a post on that - http://pedrorijo.com/blog/scala-json/
It approaches how to read json to case classes. Specifically, reading optional fields from json, on the last example:
case class User(username: String, friends: Int, enemies: Int, isAlive: Option[Boolean])
object User {
import play.api.libs.functional.syntax._
import play.api.libs.json._
implicit val userReads: Reads[User] = (
(JsPath \ "username").read[String] and
(JsPath \ "friends").read[Int] and
(JsPath \ "enemies").read[Int] and
(JsPath \ "is_alive").readNullable[Boolean]
) (User.apply _)
}
It should be enough for you to get it done.
Also, if json is a string with the desired fields, you can just write:
Json.parse(json)
so, if fullJson is the full Json object (with those undesired fields) you can just extract the firstName and lastName with fullJson \ "contact_name"
To simplify things I've done something like this (although it does create a lot of classes):
Assumptions: contact_name is optional and you want to collapse the whole thing into a single case class Contact.
{
"contact_name": {
"first_name": "hello",
"last_name": "world"
},
"phone_number": "1231231234",
"email": "test#gmail.com"
}
case class Contact(firstName: Optional[String], lastName: Optional[String], phoneNumber: String, email: String)
case class RawContactName(firstName: String, lastName: String)
case class RawContact(contactName: Optional[RawContactName], phoneNumber: String, email: String)
implicit val rawContactReads: Reads[RawContact] = (
(JsPath \ "contact_name").readNullable[RawContactName] and
(JsPath \ "phone_number").read[String] and
(JsPath \ "email").read[String]
) (RawContact.apply _)
implicit val rawContactNameReads: Reads[RawContactName] = (
(JsPath \ "first_name").read[String] and
(JsPath \ "last_name").read[String]
) (RawContactName.apply _)
def transformContact(rawContact: RawContact): Contact = {
val (maybeFirstName, maybeLastName) = rawContact.contactName match {
case Some(RawContactName(firstName, lastName)) => (Some(firstName), Some(lastName))
case None => (None, None)
}
Contact(maybeFirstName, maybeLastName, rawContact.phoneNumber, rawContact.email)
}
Effectively, I have separate case classes to represent each of the JSON nodes and a transformer function to transform the Scala JSON representation into my model class. This is effective if you have repeated values (example: multiple contact objects in the same JSON document, so multiple first_name elements would appear). Although in your specific example it might be better to skip the Raw classes and create two case classes inside your model instead: Contact and ContactName but I wanted to demonstrate a general solution which separates the View model (Raw...) from the internal Model.

Can I write Json from two model without transformers in Play Framework 2.4

I have two model classes
case class Calendar(id: String, summary: String)
case class ACL(account: String, role: String)
and i want write Json
{
"id": "some id",
"summary": "some text",
"acl": [
"user": "some user",
"role": "some role"
]
}
without Json transformers.
now I have
val calendar = ...
val acl = ...
val calendarWrite = (
(__ \ "_id").write[String] and
(__ \ "summary").write[String]
)(unlift(Calendar.unapply))
val aclWrite = (
(__ \ "user").write[String] and
(__ \ "role").write[String]
)(unlift(ACL.unapply))
val updateForMongo =
__.json.update(
(__ \ "acl" ).json.put(
JsArray( Seq( aclWrite.writes(acl) ))
)
)
calendarWrite.writes(calendar)
.transform(updateForMongo)
.fold(
invalid =>
Future.successful(0),
valid =>
calendarsCollection.insert(valid).map(l => l.n)
)
Is there a possibility to write into the write stream multiple objects?
And what about "one field" model class? Can i write custom Write, are there any workarounds?
it's a simple question if you forget about all sorts of transformers and more common terms and see in the JsObject source.
++, -, +, deepMerge that's all you need.
calendarWrite.writes(calendar) + ("acl" -> JsArray(Seq( aclWrite.writes(acl) )))
And for "one field" model class:
case class NotificationSettings(notifications: Seq[Notification])
val nwrite = new Writes[NotificationSettings]{
override def writes(o: NotificationSettings): JsValue = {
Json.obj("notifications" -> o.notifications)
}
}

play-json: how to get value of "dynamic" key using Reads[T]?

I have a Seq of JsValue elements. Each element represents the following JSON structure with two fields:
{
"name": "xy"
"key ∈ {A,B,C}": ["// some values in an array"]
}
What this means is that I know the key of the first field (always "name"), but not the key of the array since it is "dynamic". But: the possible keys are known, it is either "A", "B" or "C".
What I want to do is to map each of these JsValue objects to a case class:
case class Element(name: String, values: Seq[String])
As you can see, the name of the dynamic key is not even important. I just want to get the array that is associated with it.
But: how can I fetch the array with Reads[T] if its key differs?
implicit val reads: Reads[Element] = (
(__ \ "name").read[String] and
(__ \ "???").read[Seq[String]]
)(Element.apply _)
Or does this have to be done "manually", if yes, how?
As the other answer notes, orElse works here, but if you want more flexibility you can always write something like a method that returns a Reads that looks for a key that satisfies some predicate:
import play.api.libs.json._
def findByKey[A: Reads](p: String => Boolean): Reads[A] = Reads[A] {
case JsObject(fields) => fields.find(kv => p(kv._1)).map(
_._2.validate[A]
).getOrElse(JsError("No valid field key"))
case _ => JsError("Not an object")
}
And then:
import play.api.libs.functional.syntax._
case class Element(name: String, values: Seq[String])
object Element {
implicit val reads: Reads[Element] = (
(__ \ "name").read[String] and findByKey[Seq[String]](Set("A", "B", "C"))
)(Element.apply _)
}
And finally:
scala> Json.parse("""{ "name": "foo", "A": ["bar", "baz"] }""").asOpt[Element]
res0: Option[Element] = Some(Element(foo,List(bar, baz)))
scala> Json.parse("""{ "name": "foo", "A": [1, 2] }""").asOpt[Element]
res1: Option[Element] = None
Which approach you choose is a matter of taste, and will probably depend in part on whether the more general findByKey is useful to you in other contexts.
You can use orElse method
case class Element(name: String, values: Seq[String])
object Element {
implicit val reads: Reads[Element] = (
(__ \ "name").read[String] and
(__ \ "a").read[Seq[String]]
.orElse((__ \ "b").read[Seq[String]])
.orElse((__ \ "c").read[Seq[String]])
)(Element.apply _)
}

If statements within Play/Scala JSON parsing?

Is there a way to perform conditional logic while parsing json using Scala/Play?
For example, I would like to do something like the following:
implicit val playlistItemInfo: Reads[PlaylistItemInfo] = (
(if(( (JsPath \ "type1").readNullable[String]) != null){ (JsPath \ "type1" \ "id").read[String]} else {(JsPath \ "type2" \ "id").read[String]}) and
(JsPath \ "name").readNullable[String]
)(PlaylistItemInfo.apply _)
In my hypothetical JSON parsing example, there are two possible ways to parse the JSON. If the item is of "type1", then there will be a value for "type1" in the JSON. If this is not present in the JSON or its value is null/empty, then I would like to read the JSON node "type2" instead.
The above example does not work, but it gives you the idea of what I am trying to do.
Is this possible?
The proper way to do this with JSON combinators is to use orElse. Each piece of the combinator must be a Reads[YourType], so if/else doesn't quite work because your if clause doesn't return a Boolean, it returns Reads[PlaylistItemInfo] checked against null which will always be true. orElse let's us combine one Reads that looks for the type1 field, and a second one that looks for the type2 field as a fallback.
This might not follow your exact structure, but here's the idea:
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class PlaylistItemInfo(id: Option[String], tpe: String)
object PlaylistItemInfo {
implicit val reads: Reads[PlaylistItemInfo] = (
(__ \ "id").readNullable[String] and
(__ \ "type1").read[String].orElse((__ \ "type2").read[String])
)(PlaylistItemInfo.apply _)
}
// Read type 1 over type 2
val js = Json.parse("""{"id": "test", "type1": "111", "type2": "2222"}""")
scala> js.validate[PlaylistItemInfo]
res1: play.api.libs.json.JsResult[PlaylistItemInfo] = JsSuccess(PlaylistItemInfo(Some(test),111),)
// Read type 2 when type 1 is unavailable
val js = Json.parse("""{"id": "test", "type2": "22222"}""")
scala> js.validate[PlaylistItemInfo]
res2: play.api.libs.json.JsResult[PlaylistItemInfo] = JsSuccess(PlaylistItemInfo(Some(test),22222),)
// Error from neither
val js = Json.parse("""{"id": "test", "type100": "fake"}""")
scala> js.validate[PlaylistItemInfo]
res3: play.api.libs.json.JsResult[PlaylistItemInfo] = JsError(List((/type2,List(ValidationError(error.path.missing,WrappedArray())))))

Scala object to Json Formatter using combinators

I have implemented a class following Scala documentation
case class Creature(
name: String,
isDead: Boolean,
weight: Float,
dob: java.sql.Date
)
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val creatureFormat = (
(__ \ "name").format[String] and
(__ \ "isDead").format[Boolean] and
(__ \ "weight").format[Float] and
(__ \ "dob").format[java.sql.Date]
)(Creature.apply, unlift(Creature.unapply))
Then I call the json wrapper like this Json.toJson(Creature("John Doe", false, 100.0, new java.sql.Date(1363456800000))) and expect to see an output like {"name": "John Doe", "isDead": false, "weight": 100.0, "dob": "2013-03-17"}. Instead, I am getting an output like {"name": "John Doe", "isDead": false, "weight": 100.0, "dob": 1363456800000}.
Please note that, in the database, I can see the dob as 2013-03-17.
By default the java.util.Date Json serializer produces a number containing the date timestamp.
Alternatively, you can use a date serializer that produces a String containing a representation of the date. However, because there is no standard representation of dates in JSON, you have to explicitly supply the pattern to use to produce the text representation:
implicit val creatureFormat = (
(__ \ "name").format[String] and
(__ \ "isDead").format[Boolean] and
(__ \ "weight").format[Float] and
(__ \ "dob").format(sqlDateWrites("YYYY-MM-DD"))(sqlDateReads("YYYY-MM-DD"))
)(Creature.apply, unlift(Creature.unapply))
Here's how I resolved it (I explicitly defined apply and unapply methods)
val sdf = new java.text.SimpleDateFormat("yyyy-MM-dd")
implicit val creatureFormat = (
(__ \ "name").format[String] and
(__ \ "isDead").format[Boolean] and
(__ \ "weight").format[Float] and
(__ \ "dob").format[String])
(((name, isDead, weight, dob) => Creature(name, isDead, weight, new java.sql.Date(sdf.parse(dob).getTime()))),
unlift((cr: Creature) => Some(cr.name, cr.isDead, cr.weight, sdf.format(cr.dob))))
I do not know whether there is any better solutions.
Update
Finally, I implemented a formatter for java.sql.Date
import play.api.libs.json._
import play.api.libs.functional.syntax._
import play.api.data.validation.ValidationError
import play.api.libs.json.{ Json => PlayJson, _ }
case class Creature(
name: String,
isDead: Boolean,
weight: Float,
dob: java.sql.Date
)
implicit val sqlDateWrite = new Format[SqlDate] {
def reads(json: JsValue) = json match {
case JsString(d) => {
val theDate = new SqlDate(sdf.parse(d).getTime)
if (d.matches(sdfPattern) && theDate.compareTo(new Date(0)) > 0) JsSuccess(new SqlDate(sdf.parse(d).getTime))
else JsError(Seq(JsPath() -> Seq(ValidationError("validate.error.expected.date.in.format(dd-MM-yyyy)"))))
}
case _ => JsError(Seq(JsPath() -> Seq(ValidationError("validate.error.expected.date.in.String"))))
}
def writes(sd: SqlDate): JsValue = JsString(sdf.format(sd))
}
implicit val creatureFormat = PlayJson.format[Creature]
Now, both these lines works
val mcJson = PlayJson.toJson(Creature("John Doe", false, 100, new SqlDate(1368430000000L)))
val mcObj = PlayJson.fromJson[Creature](PlayJson.obj("name"-> "Abul Khan", "isDead"-> true, "weight"-> 115, "dob"-> "17-05-2011")).getOrElse(null)
Since you expect strings you'd have to convert everything to string and lose typing.
Json.toJson(
Creature(
"John Doe", "false", "100.0",(new java.sql.Date(1363456800000)).toString
)
)