PlayJson Scala Read Nullable Fields - json

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.

Related

Convert JSON to case class with a nested objects using Scala/Play

Say the JSON response I'm working with is formatted as follows:
[
{
"make": "Tesla",
"model": "Model S",
"year": 2017,
"color": "red",
"owner": "Bob",
"max_speed": 200,
"wheel_size": 30,
"is_convertible": true,
"license": "ABC123",
"cost": 50000,
"down_payment": 2500,
"other_property_1": 1,
"other_property_2": 2,
"other_property_3": 3,
"other_property_4": 4,
"other_property_5": 5,
"other_property_6": 6,
"other_property_7": 7,
"other_property_8": 8,
"other_property_9": 9,
"other_property_10": 10,
"other_property_11": 11
}
]
The JSON here is an array of car objects (just 1 for simplicity), and I am trying to convert this into a model using a JSON Reads converter. Let's say I have a Car case class to represent each object, and that class has has a nested FinancialInfo case class to split up the amount of attributes logically, so to avoid Scala's 22 parameter limit.
import play.api.libs.functional.syntax._
import play.api.libs.json._
case class Car(
make: String,
model: String,
year: Int,
color: String,
owner: String,
maxSpeed: Int,
wheelSize: Int,
isConvertible: Boolean,
license: String,
financialInfo: FinancialInfo, // nested case class to avoid 22 param limit
otherProperty1: Int,
otherProperty2: Int,
otherProperty3: Int,
otherProperty4: Int,
otherProperty5: Int,
otherProperty6: Int,
otherProperty7: Int,
otherProperty8: Int,
otherProperty9: Int,
otherProperty10: Int,
otherProperty11: Int
)
object Car {
implicit val reads: Reads[Car] = (
(__ \ "make").read[String] and
(__ \ "model").read[String] and
(__ \ "year").read[Int] and
(__ \ "color").read[String] and
(__ \ "owner").read[String] and
(__ \ "max_speed").read[Int] and
(__ \ "wheel_size").read[Int] and
(__ \ "is_convertible").read[Boolean] and
(__ \ "license").read[String] and
(__ \ "financialInfo").read[FinancialInfo] and
(__ \ "other_property_1").read[Int] and
(__ \ "other_property_2").read[Int] and
(__ \ "other_property_3").read[Int] and
(__ \ "other_property_4").read[Int] and
(__ \ "other_property_5").read[Int] and
(__ \ "other_property_6").read[Int] and
(__ \ "other_property_7").read[Int] and
(__ \ "other_property_8").read[Int] and
(__ \ "other_property_9").read[Int] and
(__ \ "other_property_10").read[Int] and
(__ \ "other_property_11").read[Int]
)(Car.apply _)
}
case class FinancialInfo(
cost: BigDecimal,
downPayment: BigDecimal
)
object FinancialInfo {
implicit val reads: Reads[FinancialInfo] = (
(__ \ "cost").read[BigDecimal] and
(__ \ "down_payment").read[BigDecimal]
)(FinancialInfo.apply _)
}
However, I'm guessing since there is no property in the JSON called financialInfo, it is not parsing it correctly. In my real application, I'm getting this error when I use response.json.validate[List[Car]]:
JsError(List(((0)/financialInfo,List(JsonValidationError(List(error.path.missing),WrappedArray())))))
To summarize, in the example, cost and down_payment are not contained in a nested object, even though for the Car case class I had to include a nested model called financialInfo. What is the best way to work around this error and make sure the values for cost and down_payment can be parsed? Any help or insight would be greatly appreciated!
Reads can be combined and included into each other.
So, having:
implicit val fiReads: Reads[FinancialInfo] = (
(JsPath \ "cost").read[BigDecimal] and
(JsPath \ "down_payment").read[BigDecimal]
)(FinancialInfo.apply _)
We can include it into the parent Reads:
implicit val carReads: Reads[Car] = (
(JsPath \ "make").read[String] and
(JsPath \ "model").read[String] and
fiReads // <--- HERE!
)(Car.apply _)
Now, with the following JSON:
private val json =
"""
|[
| {
| "make": "Tesla",
| "model": "Model S",
| "cost": 50000,
| "down_payment": 2500
| },
| {
| "make": "Tesla",
| "model": "Model D",
| "cost": 30000,
| "down_payment": 1500
| }
|]
""".stripMargin
val parsedJsValue = Json.parse(json)
val parsed = Json.fromJson[List[Car]](parsedJsValue)
println(parsed)
It is parsed properly:
JsSuccess(List(Car(Tesla,Model S,FinancialInfo(50000,2500)), Car(Tesla,Model D,FinancialInfo(30000,1500))),)
p.s. The Reads in the original question do no need to be wrapped into different objects. Related implicit values would be better inside same scope, closer to where they are actually used.

Dynamic Json values in Play Framework Json

I am currently using the Play framework json parser in order to parse a json string in my scala code.
I have the following class:
case class Address(address: String,
gps: GPS,
country: String) {}
object Address {
implicit val reads: Reads[Address] = (
(JsPath \ "address").read[String] and
(JsPath \ "gps").read[GPS] and
(JsPath \ "country").read[String]
) (Address.apply _)
implicit val writes: Writes[Address] = (
(JsPath \ "address").write[String] and
(JsPath \ "gps").write[GPS] and
(JsPath \ "country").write[String]
) (unlift(Address.unapply))
}
Which works fine with the following json:
{
"address": "123 Fake Street",
"country": "USA",
"gps": { ... }
}
The problem is that in some situations the json may instead have the gps field be a string which doesnt parse, i.e.
{
"address": "123 Fake Street",
"country": "USA",
"gps": "123abc"
}
Now I know that I cant have the gps member be both a string or a GPS object, but is there any way to have it be say an Option[GPS] and only have a value if the json contained a gps object?
Only very little is needed to be changed in your impl.
You need to read the field "gps" as something that is 'safe' like JsValue and then try to map it into your GPS case class if it can be done, if not, return None.
case class GPS(a:String, b:String)
object GPS {
val travelInfoReads = Json.reads[GPS]
val travelInfoWrites = Json.writes[GPS]
implicit val travelInfoFormat: Format[GPS] = Format(travelInfoReads, travelInfoWrites)
}
case class Address(address: String,
gps: Option[GPS],
country: String) {}
object Address {
implicit val reads: Reads[Address] = (
(JsPath \ "address").read[String] and
(JsPath \ "gps").read[JsValue].map(js => js.asOpt[GPS]) and
(JsPath \ "country").read[String]
) (Address.apply _)
implicit val writes: Writes[Address] = (
(JsPath \ "address").write[String] and
(JsPath \ "gps").writeNullable[GPS] and
(JsPath \ "country").write[String]
) (unlift(Address.unapply))
}
I also tested it:
val json = Json.toJson(Address("1",Some(GPS("a","b")),"2"))
println(json)
println(json.as[Address])
val newObj: JsObject = (json.as[JsObject] - "gps") + ("gps" -> JsNumber(1))
println(newObj)
val a = newObj.as[Address]
println(a)
a must beEqualTo(Address("1",None,"2"))
Output was like
{"address":"1","gps":{"a":"a","b":"b"},"country":"2"}
Address(1,Some(GPS(a,b)),2)
{"address":"1","country":"2","gps":1}
Address(1,None,2)

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 Framework: How to Validate Optional Fields When Parsing 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))))))

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