Parsing JSON Date Time in Scala/Play - json

I have the following Read defined:
import org.joda.time.DateTime;
implicit val userInfoRead: Reads[UserInfo] = (
(JsPath \ "userName").readNullable[String] and
] (JsPath \ "startDate").readNullable[DateTime]
(UserInfo.apply _)
With the following JSON object being passed in:
"userInfo" : {
"userName": "joeuser",
"startDate": "2006-02-28"
}
When I validate this data I get the following error:
(/startDate,List(ValidationError(validate.error.expected.jodadate.format,WrappedArray(yyyy-MM-dd))))))
Any suggestions on what I'm missing in the formatting?

As far as I can see, the issue is probably just the format not matching what Joda is expecting. I simplified a bit, and this worked for me:
scala> import org.joda.time.DateTime
import org.joda.time.DateTime
scala> case class UserInfo(userName: String, startDate: DateTime)
defined class UserInfo
scala> implicit val dateReads = Reads.jodaDateReads("yyyy-MM-dd")
dateReads: play.api.libs.json.Reads[org.joda.time.DateTime] = play.api.libs.json.DefaultReads$$anon$10#22db02cb
scala> implicit val userInfoReads = Json.reads[UserInfo]
userInfoReads: play.api.libs.json.Reads[UserInfo] = play.api.libs.json.Reads$$anon$8#52bcbd5d
scala> val json = Json.parse("""{
| "userName": "joeuser",
| "startDate": "2006-02-28"
| }""")
json: play.api.libs.json.JsValue = {"userName":"joeuser","startDate":"2006-02-28"}
scala> json.validate[UserInfo]
res12: play.api.libs.json.JsResult[UserInfo] = JsSuccess(UserInfo(joeuser,2006-02-28T00:00:00.000-05:00),)

Related

Scala Play - Json Parse

I am receiving a JSON from an external service and my goal is to parse it exactly as it is.
The main issue is this: a value can be nullable or it can be absent BUT null has different meaning of absent. So I want to catch this somehow.
For example this JSON:
{
"a": null,
"b": 1
}
is different from this one:
{
"b": 1
}
Can you help me please?
UPDATE:
Sorry for the delay in the update. Anyway: you are right, I have a implicit custom reads in the middle and currently I use "a".readNullable[Double] and "a".write[Option[Double]] and case class is something like:
case class Example(a: Option[Double])
Just laying out what #mfirry was talking about with a detailed example (play-json 2.6):
scala> import play.api.libs.json._
import play.api.libs.json._
scala> val json1 = Json.parse("""{"a": null, "b": 1}""")
json1: play.api.libs.json.JsValue = {"a":null,"b":1}
scala> val json2 = Json.parse("""{"b": 1}""")
json2: play.api.libs.json.JsValue = {"b":1}
scala> (json1 \ "a").isDefined
res8: Boolean = true
scala> (json1 \ "a") == JsDefined(JsNull)
res3: Boolean = true
scala> (json2 \ "a").isDefined
res7: Boolean = false
scala> (json2 \ "a")
res5: play.api.libs.json.JsLookupResult = JsUndefined('a' is undefined on object: {"b":1})

Convert Json to Joda DateTime in Play! scala 2.5

I have read this question (and the other ones on SO), but I still don't manage to convert a JsValue to a case class with a Joda DateTime.
Here is the JSON I have:
val json = Json.parse(
"""
{
"message": "Bla bla bla",
"updated_time": "2016-09-17T12:48:12+0000"
}
"""
)
And the corresponding case class is:
import org.joda.time.DateTime
final case class FacebookPost(message: String, updated_time: DateTime)
I also added this implicit DateTime reader (I tried some other as well):
implicit val readsJodaLocalDateTime = Reads[DateTime](js =>
js.validate[String].map[DateTime](dtString => new DateTime(dtString)))
But when I try to convert my json to the corresponding case class (json.as[FacebookPost]), I get this error:
play.api.libs.json.JsResultException: `JsResultException(errors:List((/updated_time,List(ValidationError(List(error.expected.jodadate.format),WrappedArray(yyyy-MM-dd))))))`
What am I doing wrong?
Your problem is the format of your datetime: 2016-09-17T12:48:12+0000.
By default, Joda's DateTime class can't automatically figure out the format you're using, so you have to provide a hint using DateTimeFormat.
Example for our custom Reads[DateTime]:
import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._
val dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
val customReads = Reads[DateTime](js =>
js.validate[String].map[DateTime](dtString =>
DateTime.parse(dtString, DateTimeFormat.forPattern(dateFormat))
)
)
implicit val reads: Reads[FacebookPost] = (
(__ \ "message").read[String] and
(__ \ "updated_time").read[DateTime](customReads)
)(FacebookPost.apply _)
Then, it's possible to do the following:
val json = Json.parse(
"""
{
"message": "Bla bla bla",
"updated_time": "2016-09-17T12:48:12+0000"
}
""")
val post = json.validate[FacebookPost]
// => JsSuccess(FacebookPost(Bla bla bla,2016-09-17T14:48:12.000+02:00))
edit:
You don't need to create the Read[DateTime] from scratch, there is a predefined method in Play that helps you with that:
object CustomReads /* or wherever you want to define it */ {
val dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
implicit val jodaDateReads = play.api.libs.json.Reads.jodaDateReads(dateFormat)
}
Done. Just import whenever you need it, e.g. in the companion object of your model classes:
object FacebookPost {
import utils.CustomReads._
implicit val reads: Reads[FacebookPost] = Json.reads[FacebookPost]
}
You can "override" the implicit Read[DateTime] provided by Play by exluding it from your imports:
import play.api.libs.json.Reads.{DefaultJodaDateReads => _, _}

Reads with validation for email or empty string in Play/Scala

I am trying to create a validator for reads that will only allow a valid email or an empty string.
What I tried until now seem to work only partially.
Here is my reads defenition:
case class EmailFieldValueForm(value: String) extends FieldValueForm
val emailFieldValueFormReads: Reads[EmailFieldValueForm] =
(__ \ "value").read[String](email or maxLength(0)).map(EmailFieldValueForm.apply _)
When I test it, I am getting the following error:
diverging implicit expansion for type play.api.libs.json.Reads[V]
[error] starting with value uuidReads in trait DefaultReads [error]
(__ \ "value").read[String](email or
maxLength(0)).map(EmailFieldValueForm.apply _)
Also, tried doing it with regex:
val emailFieldValueFormReads: Reads[EmailFieldValueForm] =
(__ \ "value").read[String](email or pattern("""^$"""r)).map(EmailFieldValueForm.apply)
And in this case, any json I provide is passing.For example:
val invalidJson = Json.parse(
"""
|{
| "value": "boo"
|}
""".stripMargin
Simply gives me an empty value, but does not fail validation.
What am I doing wrong?
Thanks,
SOLUTION
Seems like I was not doing the testing correctly. The following worked:
val invalidJson = Json.parse(
"""
|{
| "value": "boo"
|}
""".stripMargin
)
(EmailFieldValueForm.emailFieldValueFormReads reads invalidJson match {
case JsSuccess(value, _) => value
case JsError(e) => throw JsResultException(e)
}) must throwA[JsResultException]
The problem lies in implicit reads: Reads[M] for def maxLength[M](m: Int)(implicit reads: Reads[M], p: M => scala.collection.TraversableLike[_, M])
You may specify type M explicitly and all will be fine
import play.api.libs.functional.syntax._
import play.api.libs.json.Reads._
import play.api.libs.json._
val emailFieldValueFormReads: Reads[EmailFieldValueForm] =
((__ \ "value").read[String](email or maxLength[String](0))).map(EmailFieldValueForm.apply _)
Json.parse("""{"value": "sss"}""").validate[EmailFieldValueForm](emailFieldValueFormReads)
Json.parse("""{"value": ""}""").validate[EmailFieldValueForm](emailFieldValueFormReads)
Json.parse("""{"value": "a#a.com"}""").validate[EmailFieldValueForm](emailFieldValueFormReads)
scala> res0: play.api.libs.json.JsResult[EmailFieldValueForm] = JsError(List((/value,List(ValidationError(List(error.email),WrappedArray()), ValidationError(List(error.maxLength),WrappedArray(0))))))
scala> res1: play.api.libs.json.JsResult[EmailFieldValueForm] = JsSuccess(EmailFieldValueForm(),/value)
scala> res2: play.api.libs.json.JsResult[EmailFieldValueForm] = JsSuccess(EmailFieldValueForm(a#a.com),/value)
For testing with scalatest (PlaySpec) you may use
Json.parse("""{"value": "sss"}""").validate[EmailFieldValueForm](emailFieldValueFormReads) must be an 'error
Json.parse("""{"value": "a#a.com"}""").validate[EmailFieldValueForm](emailFieldValueFormReads) must be an 'success
Json.parse("""{"value": ""}""").validate[EmailFieldValueForm](emailFieldValueFormReads) must be an 'success

Parsing a JSArray object into a list of objects in Scala Play

I have the following JSON:
[{"id_str":"67979542","name":"account"}, {"id_str":"12345678","name":"account2"}, {"id_str":"3423423423","name":"account3"}]
which has been parsed into a play.api.libs.json.JsArray object with 3 elements.
I want to parse this JsArray into my a custom object Group with the following code:
case class Group(id: String, name: String)
implicit val twitterGroupReads: Reads[Group] = (
(JsPath \\ "id_str").read[String] and
(JsPath \\ "name").read[String]
)(Group.apply _)
But I don't know how to use the library to get all the elements from the array and parse those into my custom object.
The Play JSON framework has a number of built-in objects for handling JSON, among which is Reads.traversableReads, which will be used implicitly to deserialize collections of other types for which a Reads object can be found implicitly. And you wrote a proper Reads object. So unless I'm missing something, you're good to go:
scala> import play.api.libs.json._
import play.api.libs.json._
scala> import play.api.libs.functional.syntax._
import play.api.libs.functional.syntax._
scala> case class Group(id: String, name: String)
defined class Group
scala> implicit val twitterGroupReads: Reads[Group] = (
| (JsPath \\ "id_str").read[String] and
| (JsPath \\ "name").read[String]
| )(Group.apply _)
twitterGroupReads: play.api.libs.json.Reads[Group] = play.api.libs.json.Reads$$anon$8#f2fae02
scala> val json = Json.parse("""[{"id_str":"67979542","name":"account"}, {"id_str":"12345678","name":"account2"}, {"id_str":"3423423423","name":"account3"}]""")
json: play.api.libs.json.JsValue = [{"id_str":"67979542","name":"account"},{"id_str":"12345678","name":"account2"},{"id_str":"3423423423","name":"account3"}]
scala> json.as[Seq[Group]]
res0: Seq[Group] = List(Group(67979542,account), Group(12345678,account2), Group(3423423423,account3))

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