How to serialize Scala Map<String, MyObj> in Play Framework - json

So I basically have code like:
case class MyObj( value:String, unit: String)
case class ObjSeries( id: String, myObjs: Map[String, MyObj] )
However, creating writers like:
implicit val myObjWrites = Json.writes[MyObj]
implicit val objSeriesWrites: Writes[ObjSeries] = (
(JsPath \ "id").write[String] and
(JsPath \ "myObjs").write[Map[String, MyObj]]
)(unlift(ObjSeries.unapply))
Fails with " overloaded method value apply with alternatives - cannot be applied to...".
What am I missing?
The above is of course a simple example; the actual data structures I'm working on are a bit more complex. The actual example is a class:
case class ObservationSeries(
sourceId: Option[String],
geometry: Option[Point],
levels: Option[Seq[Level]],
referenceTime: Option[String],
observations: Option[Map[String, Observation]]
)
And the error message goes:
overloaded method value apply with alternatives:
[error] [B](f: B => (Option[String], Option[no.met.geometry.Point], Option[Seq[no.met.geometry.Level]], Option[String], Option[scala.collection.immutable.Map[String,models.Observation]]))(implicit fu: play.api.libs.functional.ContravariantFunctor[play.api.libs.json.OWrites])play.api.libs.json.OWrites[B] <and>
[error] [B](f: (Option[String], Option[no.met.geometry.Point], Option[Seq[no.met.geometry.Level]], Option[String], Option[scala.collection.immutable.Map[String,models.Observation]]) => B)(implicit fu: play.api.libs.functional.Functor[play.api.libs.json.OWrites])play.api.libs.json.OWrites[B]
[error] cannot be applied to (models.ObservationSeries => (Option[String], Option[no.met.geometry.Point], Option[Seq[no.met.geometry.Level]], Option[String], Option[scala.collection.Map[String,models.Observation]]))
[error] (JsPath \ "referenceTime").writeNullable[String] and

Not sure if it was a copy error, but the first class is missing the name of the second field:
# ↓↓↓
case class ObjSeries(id: String, Map[String, MyObj] )
Apart from that, what you have should work. Are you sure is this code that is failing?

Related

Scala reads object with nested optional property

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

how to add custom ValidationError in Json Reads in PlayFramework

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)

Play framework 2.5 reads optional filter

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.

Play JSON: Reading and validating a JsObject with unknown keys

I'm reading a nested JSON document using several Reads[T] implementations, however, I'm stuck with the following sub-object:
{
...,
"attributes": {
"keyA": [1.68, 5.47, 3.57],
"KeyB": [true],
"keyC": ["Lorem", "Ipsum"]
},
...
}
The keys ("keyA", "keyB"...) as well as the amount of keys are not known at compile time and can vary. The values of the keys are always JsArray instances, but of different size and type (however, all elements of a particular array must have the same JsValue type).
The Scala representation of one single attribute:
case class Attribute[A](name: String, values: Seq[A])
// 'A' can only be String, Boolean or Double
The goal is to create a Reads[Seq[Attribute]] that can be used for the "attributes"-field when transforming the whole document (remember, "attributes" is just a sub-document).
Then there is a simple map that contains allowed combinations of keys and array types that should be used to validate attributes. Edit: This map is specific for each request (or rather specific for every type of json document). But you can assume that it is always available in the scope.
val required = Map(
"KeyA" -> "Double",
"KeyB" -> "String",
"KeyD" -> "String",
)
So in the case of the JSON shown above, the Reads should create two errors:
"keyB" does exist, but has the wrong type (expected String, was boolean).
"keyD" is missing (whereas keyC is not needed and can be ignored).
I'm having trouble creating the necessary Reads. The first thing I tried as a first step, from the perspective of the outer Reads:
...
(__ \ "attributes").reads[Map[String, JsArray]]...
...
I thought this is a nice first step because if the JSON structure is not an object containing Strings and JsArrays as key-value pairs, then the Reads fails with proper error messages. It works, but: I don't know how to go on from there. Of course I just could create a method that transforms the Map into a Seq[Attribute], but this method somehow should return a JsResult, since there are further validations to do.
The second thing I tried:
val attributeSeqReads = new Reads[Seq[Attribute]] {
def reads(json: JsValue) = json match {
case JsObject(fields) => processAttributes(fields)
case _ => JsError("attributes not an object")
}
def processAttributes(fields: Map[String, JsValue]): JsResult[Seq[Attribute]] = {
// ...
}
}
The idea was to validate each element of the map manually within processAttributes. But I think this is too complicated. Any help is appreciated.
edit for clarification:
At the beginning of the post I said that the keys (keyA, keyB...) are unknown at compile time. Later on I said that those keys are part of the map required which is used for validation. This sounds like a contradiction, but the thing is: required is specific for each document/request and is also not known at compile time. But you don't need to worry about that, just assume that for every request the correct required is already available in the scope.
You are too confused by the task
The keys ("keyA", "keyB"...) as well as the amount of keys are not known at compile time and can vary
So the number of keys and their types are known in advance and the final?
So in the case of the JSON shown above, the Reads should create two
errors:
"keyB" does exist, but has the wrong type (expected String, was
boolean).
"keyD" is missing (whereas keyC is not needed and can be ignored).
Your main task is just to check the availability and compliance?
You may implement Reads[Attribute] for every your key with Reads.list(Reads.of[A]) (this Reads will check type and required) and skip omitted (if not required) with Reads.pure(Attribute[A]). Then tuple convert to list (_.productIterator.toList) and you will get Seq[Attribute]
val r = (
(__ \ "attributes" \ "keyA").read[Attribute[Double]](list(of[Double]).map(Attribute("keyA", _))) and
(__ \ "attributes" \ "keyB").read[Attribute[Boolean]](list(of[Boolean]).map(Attribute("keyB", _))) and
((__ \ "attributes" \ "keyC").read[Attribute[String]](list(of[String]).map(Attribute("keyC", _))) or Reads.pure(Attribute[String]("keyC", List()))) and
(__ \ "attributes" \ "keyD").read[Attribute[String]](list(of[String]).map(Attribute("keyD", _)))
).tupled.map(_.productIterator.toList)
scala>json1: play.api.libs.json.JsValue = {"attributes":{"keyA":[1.68,5.47,3.57],"keyB":[true],"keyD":["Lorem","Ipsum"]}}
scala>res37: play.api.libs.json.JsResult[List[Any]] = JsSuccess(List(Attribute(keyA,List(1.68, 5.47, 3.57)), Attribute(KeyB,List(true)), Attribute(keyC,List()), Attribute(KeyD,List(Lorem, Ipsum))),)
scala>json2: play.api.libs.json.JsValue = {"attributes":{"keyA":[1.68,5.47,3.57],"keyB":[true],"keyC":["Lorem","Ipsum"]}}
scala>res38: play.api.libs.json.JsResult[List[Any]] = JsError(List((/attributes/keyD,List(ValidationError(List(error.path.missing),WrappedArray())))))
scala>json3: play.api.libs.json.JsValue = {"attributes":{"keyA":[1.68,5.47,3.57],"keyB":["Lorem"],"keyC":["Lorem","Ipsum"]}}
scala>res42: play.api.libs.json.JsResult[List[Any]] = JsError(List((/attributes/keyD,List(ValidationError(List(error.path.missing),WrappedArray()))), (/attributes/keyB(0),List(ValidationError(List(error.expected.jsboolean),WrappedArray())))))
If you will have more than 22 attributes, you will have another problem: Tuple with more than 22 properties.
for dynamic properties in runtime
inspired by 'Reads.traversableReads[F[_], A]'
def attributesReads(required: Map[String, String]) = Reads {json =>
type Errors = Seq[(JsPath, Seq[ValidationError])]
def locate(e: Errors, idx: Int) = e.map { case (p, valerr) => (JsPath(idx)) ++ p -> valerr }
required.map{
case (key, "Double") => (__ \ key).read[Attribute[Double]](list(of[Double]).map(Attribute(key, _))).reads(json)
case (key, "String") => (__ \ key).read[Attribute[String]](list(of[String]).map(Attribute(key, _))).reads(json)
case (key, "Boolean") => (__ \ key).read[Attribute[Boolean]](list(of[Boolean]).map(Attribute(key, _))).reads(json)
case _ => JsError("")
}.iterator.zipWithIndex.foldLeft(Right(Vector.empty): Either[Errors, Vector[Attribute[_ >: Double with String with Boolean]]]) {
case (Right(vs), (JsSuccess(v, _), _)) => Right(vs :+ v)
case (Right(_), (JsError(e), idx)) => Left(locate(e, idx))
case (Left(e), (_: JsSuccess[_], _)) => Left(e)
case (Left(e1), (JsError(e2), idx)) => Left(e1 ++ locate(e2, idx))
}
.fold(JsError.apply, { res =>
JsSuccess(res.toList)
})
}
(__ \ "attributes").read(attributesReads(Map("keyA" -> "Double"))).reads(json)
scala> json: play.api.libs.json.JsValue = {"attributes":{"keyA":[1.68,5.47,3.57],"keyB":[true],"keyD":["Lorem","Ipsum"]}}
scala> res0: play.api.libs.json.JsResult[List[Attribute[_ >: Double with String with Boolean]]] = JsSuccess(List(Attribute(keyA,List(1.68, 5.47, 3.57))),/attributes)

How to send Json from client with missing fields for its corresponding Case Class after using Json.format function

I have a case Class and its companion object like below. Now, when I send JSON without id, createdAt and deletedAt fields, because I set them elsewhere, I get [NoSuchElementException: JsError.get] error. It's because I do not set above properties.
How could I achieve this and avoid getting the error?
case class Plan(id: String,
companyId: String,
name: String,
status: Boolean = true,
#EnumAs planType: PlanType.Value,
brochureId: Option[UUID],
lifePolicy: Seq[LifePolicy] = Nil,
createdAt: DateTime,
updatedAt: DateTime,
deletedAt: Option[DateTime]
)
object Plan {
implicit val planFormat = Json.format[Plan]
def fromJson(str: JsValue): Plan = Json.fromJson[Plan](str).get
def toJson(plan: Plan): JsValue = Json.toJson(plan)
def toJsonSeq(plan: Seq[Plan]): JsValue = Json.toJson(plan)
}
JSON I send from client
{
"companyId": "e8c67345-7f59-466d-a958-7c722ad0dcb7",
"name": "Creating First Plan with enum Content",
"status": true,
"planType": "Health",
"lifePolicy": []
}
You can introduce another case class just to handle serialization from request:
like this
case class NewPlan(name: String,
status: Boolean = true,
#EnumAs planType: PlanType.Value,
brochureId: Option[UUID],
lifePolicy: Seq[LifePolicy] = Nil
)
and then use this class to populate your Plan class.
The fundamental issue is that by the time a case class is instantiated to represent your data, it must be well-typed. To shoe horn your example data into your example class, the types don't match because some fields are missing. It's literally trying to call the constructor without enough arguments.
You've got a couple options:
You can make a model that represents the incomplete data (as grotrianster suggested).
You can make the possible missing fields Option types.
You can custom-write the Reads part of your Format to introduce intelligent values or dummy values for the missing ones.
Option 3 might look something like:
// Untested for compilation, might need some corrections
val now: DateTime = ...
val autoId = Reads[JsObject] {
case obj: JsObject => JsSuccess(obj \ 'id match {
case JsString(_) => obj
case _ => obj.transform(
__.update((__ \ 'id).json.put("")) andThen
__.update((__ \ 'createdTime).json.put(now)) andThen
__.update((__ \ 'updatedTime).json.put(now))
)
})
case _ => JsError("JsObject expected")
}
implicit val planFormat = Format[Plan](
autoId andThen Json.reads[Plan],
Json.writes[Plan])
Once you do this once, if the issue is the same for all your other models, you can probably abstract it into some Format factory utility function.
This may be slightly cleaner for autoId:
val autoId = Reads[JsObject] {
// Leave it alone if we have an ID already
case obj: JsObject if (obj \ 'id).asOpt[String].isSome => JsSuccess(obj)
// Insert dummy values if we don't have an `id`
case obj: JsObject => JsSuccess(obj.transform(
__.update((__ \ 'id).json.put("")) andThen
__.update((__ \ 'createdTime).json.put(now)) andThen
__.update((__ \ 'updatedTime).json.put(now))
))
case _ => JsError("JsObject expected")
}