I'm trying to write a reads for the following class
case class User(id: String, imageId: Option[String])
The JSON I have looks like this:
{
"id": 1
"image": { "id" : 2 }
}
However the 'image' field may not exist, or it may be null.
My reads looks like this:
implicit val userReader: Reads[User] = (
(JsPath \ "id").read[String] and
(JsPath \ "image" \ "id").readNullable[String]
) (User.apply _)
But this does not seem to work, I still get an error when it finds a sample with no image. 'ValidationError [...] error.path.missing [...] /image/id'
How can this be solved? I'd prefer not to create an Image class, which I'm not going to use anywhere.
I think the issue comes from the fact that you are trying to read a String whereas id is an Int. You should mind your types in your json / reader.
Two options
if id is really an Int, read it as an Int
if id is a String, change your JSON
If you still want to have a String in your case class, you can do :
(JsPath \ "image" \ "id").readNullable[Int].map(_.toString)
You forgot a comma between the two elements ("id" and "image"). That's why the /image/id path seems to be missing for the parser.
There are all sorts of tools for validating a JSON structure, for instance https://jsonlint.com/ .
So your JSON structure should look like this:
{
"id": 1,
"image": {
"id": 2
}
}
i may be late but here is a workaround:
implicit val userReader: Reads[User] = (
(JsPath \ "id").read[String] and
(JsPath \ "image" \ "id").readNullable[String].orElse((__ \ "dummy").readNullable[String])
) (User.apply _)
Not perfect but it work well
better solution would be to directly cast the Js Reads(JsSuccess(None:Option[Int])) inside the orElse but i haven't been able to do it...
Related
I am using play Reads validation helpers i want to show some custom message in case of json exception eg:length is minimum then specified or the given email is not valid , i knnow play displays the error message like this error.minLength but i want to display a reasonable message like please enter the character greater then 1 (or something ) here is my code
case class DirectUserSignUpValidation(firstName: String,
lastName: String,
email: String,
password: String) extends Serializable
object DirectUserSignUpValidation {
var validationErrorMsg=""
implicit val readDirectUser: Reads[DirectUserSignUpValidation] = (
(JsPath \ "firstName").read(minLength[String](1)) and
(JsPath \ "lastName").read(minLength[String](1)) and
(JsPath \ "email").read(email) and
(JsPath \ "password").read(minLength[String](8).
filterNot(ValidationError("Password is all numbers"))(_.forall(_.isDigit)).
filterNot(ValidationError("Password is all letters"))(_.forall(_.isLetter))
)) (UserSignUpValidation.apply _)
}
i have tried to add ValidationErrorlike this
(JsPath \ "email").read(email,Seq(ValidationError("email address not correct")) and
but its giving me compile time error
too many arguments for method read: (t: T)play.api.libs.json.Reads[T]
please helo how can i add custom validationError messages while reading json data
There is no such thing as (JsPath \ "firstName").read(minLength[String](1)) in play json. what you can do with custom error message is this:
(JsPath \ "firstName")
.read[String]
.filter(ValidationError("your.error.message"))(_.length > 0)
ValidationError messages are supposed to be keys to be used for translation, not human readable messages.
However, if you still want to change the message for minLength, you'll need to reimplement it, since it is hard-coded.
Thankfully, the source code is available, so you can easily change it as you please:
def minLength[M](m: Int)(implicit reads: Reads[M], p: M => scala.collection.TraversableLike[_, M]) =
filterNot[M](JsonValidationError("error.minLength", m))(_.size < m)
If you want to use a more generic pattern to specify errors, the only access you have is using the result from your validation. For instance, you could do
val json: JsValue = ???
json.validate[DirectUserSignUpValidation] match {
case JsSuccess(dusuv, _) => doSomethingWith(dusuv)
case JsError(errs) => doSomethingWithErrors(errs)
}
Or, with a more compact approach
json.validate[DirectUserSignUpValidation].
fold(doSomethingWithErrors, doSomethingWith)
I have the following reads function for parsing JSON files.
case class tables(col1 : Option[List[another case class]], col2 : Option[List[another case class]], col3 : Option[List[another case class]], col4 : Option[List[another case class]])
implicit val tablesRead: Reads[tables] = (
(JsPath \ "col1").read(Reads.optionWithNull[List[data1]]).filterNot(_.get.isEmpty) and
(JsPath \ "col2").read(Reads.optionWithNull[List[data2]]).filterNot(_.get.isEmpty) and
(JsPath \ "col3").read(Reads.optionWithNull[List[data3]]).filterNot(_.get.isEmpty) and
(JsPath \ "col4").read(Reads.optionWithNull[List[data4]]).filterNot(_.get.isEmpty)
) (tables.apply _)
I want to then insert the JSON into a database after having validated it. I have therefore declared the following function.
def createFromJson = Action.async(parse.json) { request =>
request.body.validate[jsonWrapper] match {
case JsSuccess(data, _) =>
for {
dbFuture <- dataFuture(data.userID)
lastError <- dbFuture.insert(data.tables)
} yield {
Ok("Success\n")
}
case JsError(errors) => Future.successful(BadRequest("Failed :" + Error.show(errors)))
}
}
This works and correctly rejects JSONs looking like this:
{"tables":{"col1":[],"col2":[],"col3":[],"col4":[]}, "userID":"irrelavent"}
and accepts JSONs with actual data in, like so:
{"tables":{"col1":[{data1}],"col2":[{data2}],"col3":[{data3}],"col4":[{data4}]}, "userID":"irrelavent"}
But want i need is something that does this but also accepts a JSON with missing fields
{"tables":{"col1":[{data1}],"col2":[],"col3":[{data3}],"col4":[{data4}]}, "userID":"irrelavent"}
And preferable ignore them (i.e. return something like :
{"tables":{"col1":[{data1}],"col3":[{data3}],"col4":[{data4}]}, "userID":"irrelavent"})
Is this possible to do?
Many thanks,
Peter M.
You can automatically generate a Reads[tables] using Json.reads macro with the behavior you want:
implicit val tablesRead: Reads[tables] = Json.reads[tables]
If the fields is missing from the JSON the column will be None.
On a minor note, the common form in scala is to start a class name with a capital letter so you should rename tables to Tables.
I'm trying to parse the data from GovTrack, for example, https://www.govtrack.us/api/v2/bill/74369 . But titles is in a peculiar format:
"titles": [
[
"short",
"introduced",
"Public Online Information Act of 2011"
],
[
"official",
"introduced",
"To establish an advisory committee to issue nonbinding governmentwide guidelines..."
]
]
titles is an array of each title type, with fields in a particular order. I want to read this into a more standard JSON format:
{
'short_title': "Public Online Information Act of 2011",
'official_title': "To establish an advisory committee to issue nonbinding governmentwide guidelines..."
}
The short title or official title may or may not be there, and there could actually be several short titles.
How do I make a Reads for this? Right now I've got:
implicit val billReads: Reads[Bill] = (
(JsPath \ "id").read[Int] and
(JsPath \ "display_number").read[String] and
(JsPath \ "current_status").read[String] and
(JsPath \ "titles")(0)(2).read[String]
)(Bill.apply _)
How do I specify "The member of the array that has a first element equal to 'official'"?
As far as I know, there is no out of the box way to do it, but I would do it with additional custom reader, like this:
val officialReads = new Reads[String] {
override def reads(json: JsValue): JsResult[String] = (json \ "titles") match {
case JsArray(titles) => titles.collectFirst({
case JsArray(values) if (values.headOption.map(v => "official".equals(v.as[String])).getOrElse(false)) =>
JsSuccess(values.tail.tail.head.as[String])
}).getOrElse(JsError("No official title"))
case _ => JsError("Can't read official title")
}
}
And your Bill reader would look like this:
val implicitReads: Reads[Bill] = (
(__ \ "id").read[Int] and
(__ \ "display_number").read[String] and
(__ \ "current_status").read[String] and
officialReads
)(Bill.apply _)
I've tested, this works :)
I'm trying to extract the data from a JSON file that looks like that :
val json: JsValue = Json.parse("""
{
"item": {
"id" : "11111111",
"name" : "box",
"categories" : [{
"name" : "blue box",
"id" : "12345",
},
{
"name" : "fancy box",
"id" : "98765",
}]
}
}
""")
I would like to do it using the play JSON library. I came up with this code :
//I define my class and reader for one category
case class ItemCategory(id: Option[String], name: String)
implicit val categoryRead: Reads[ItemCategory] = (
(JsPath \ "item" \ "categories" \ "id").readNullable[String] and
(JsPath \ "item" \ "categories" \ "name").read[String]
)(ItemCategory.apply _)
//I define my class and reader for one item
case class MyItem(categories : Option[List[ItemCategory]], id : Option[String], name : Option[String])
implicit val myItemRead: Reads[MyItem] = (
(JsPath \ "item" \ "categories").readNullable[List[ItemCategory]] and
(JsPath \ "item" \ "id").readNullable[String] and
(JsPath \ "item" \ "name").readNullable[String]
)(MyItem.apply _)
//I then try to read :
var item: JsResult[MyItem] = json.validate[MyItem](myItemRead)
println(item)
However this code gives a JsError :List(ValidationError(validate.error.missing-path,WrappedArray())).
Which to my understanding simply means that some path went missing. I tried to read just one category and it worked fine, I try to read an item without trying to get the categories and here again it went fine. Hence I think the problem is on reading a list of items. I would really appreciate if you could help me with this.
Path is relative. categoryRead js path should be relative. Such as _ \ xxx
I have the following Json:
{
"web-category" : "macaroons",
"sub-categories" : [
{ "name" : "pink" },
{ "name" : "blue" },
{ "name" : "green" }
]
}
I have got it in Play as a JsObject. So I can now successfully do the following:
//(o is the JsObject)
val webCat:Option[String] = (o \ "web-category").asOpt[String]
println(webCat.toString)
>> Some(macaroons)
So far, so good. But how do I access the array Json objects? I have this...
val subCats:Option[JsArray] = (o \ "sub-categories").asOpt[JsArray]
println(subCats.toString)
>> Some([{"name" : "blue"},{"name" : "green"},{"name" : "pink"}])
but what I need is to take the JsArray and get a List of all the names something like this:
List("blue", "green", "pink")
Don't know how to access the JsArray thusly.
my thanks for your help in this.
I'd argue that it's generally a good idea to move from JSON-land to native-Scala-representation-land as early as possible. If obj is your JsObject, for example, you can write this:
val subCategories = (obj \ "sub-categories").as[List[Map[String, String]]]
val names = subCategories.map(_("name"))
Or even:
case class Category(name: String, subs: List[String])
import play.api.libs.functional.syntax._
implicit val categoryReader = (
(__ \ "web-category").read[String] and
(__ \ "sub-categories").read[List[Map[String, String]]].map(_.map(_("name")))
)(Category)
And then:
obj.as[Category]
This latter approach makes error handling even cleaner (e.g. you can just replace as with asOpt at this top level) and composes nicely with other Reads type class instances—if you have a JsArray of these objects, for example, you can just write array.as[List[Category]] and get what you expect.
What Peter said, or:
(o \ "sub-categories" \\ "name").map(_.as[String]).toList
Something like this:
subCats.map( jsarray => jsarray.value.map(jsvalue => (jsvalue \ "name").as[String]).toList)
This will normally return a Option[List[String]]