Scala Form Formatter for accepting JSON values - json

I am trying to achieve the following.
val form = Form(
mapping(
"stream-name" -> nonEmptyText(minLength = 1),
"query" -> optional(nonEmptyText),
"input-stream" -> of(JsArray),
"archive-ttlsec" -> optional(longNumber),
"sample-rate" -> optional(of(play.api.data.format.Formats.doubleFormat)),
"sample-size" -> optional(number),
)(Data.apply)(Data.unapply)
)
case class Data(
`stream-name`: String,
query: Option[String],
`input-stream`: JsArray,
`archive-ttlsec`: Option[Long] = None,
`sample-rate`: Option[Double] = None,
`sample-size`: Option[Int] = None,
)
This is a Scala Form data validator that I have created with
"input-stream" -> of(JsArray)
Form is not able to validate this and is giving
Type Mismatch, expected: Formatter[NonInferedT] actual: JsArray.type
Any workaround for this?

Related

Scala - How to handle key not found in a Map when need to skip non-existing keys without defaults?

I have a set of Strings and using it as key values to get JValues from a Map:
val keys: Set[String] = Set("Metric_1", "Metric_2", "Metric_3", "Metric_4")
val logData: Map[String, JValue] = Map("Metric_1" -> JInt(0), "Metric_2" -> JInt(1), "Metric_3" -> null)
In the below method I'm parsing values for each metric. First getting all values, then filtering to get rid of null values and then transforming existing values to booleans.
val metricsMap: Map[String, Boolean] = keys
.map(k => k -> logData(k).extractOpt[Int]).toMap
.filter(_._2.isDefined)
.collect {
case (str, Some(0)) => str -> false
case (str, Some(1)) => str -> true
}
I've faced a problem when one of the keys is not found in the logData Map. So I'm geting a java.util.NoSuchElementException: key not found: Metric_4.
Here I'm using extractOpt to extract a value from a JSON and don't need default values. So probably extractOrElse will not be helpful since I only need to get values for existing keys and skip non-existing keys.
What could be a correct approach to handle a case when a key is not present in the logData Map?
UPD: I've achieved the desired result by .map(k => k -> apiData.getOrElse(k, null).extractOpt[Int]).toMap. However still not sure that it's the best approach.
That the values are JSON is a red herring--it's the missing key that's throwing the exception. There's a method called get which retrieves a value from a map wrapped in an Option. If we use Ints as the values we have:
val logData = Map("Metric_1" -> 1, "Metric_2" -> 0, "Metric_3" -> null)
keys.flatMap(k => logData.get(k).map(k -> _)).toMap
> Map(Metric_1 -> 1, Metric_2 -> 0, Metric_3 -> null)
Using flatMap instead of map means unwrap the Some results and drop the Nones. Now, if we go back to your actual example, we have another layer and that flatMap will eliminate the Metric_3 -> null item:
keys.flatMap(k => logData.get(k).flatMap(_.extractOpt[Int]).map(k -> _)).toMap
You can also rewrite this using a for comprehension:
(for {
k <- keys
jv <- logData.get(k)
v <- jv.extractOpt[Int]
} yield k -> v).toMap
I used Success and Failure in place of the JSON values to avoid having to set up a shell with json4s to make an example:
val logData = Map("Metric_1" -> Success(1), "Metric_2" -> Success(0), "Metric_3" -> Failure(new RuntimeException()))
scala> for {
| k <- keys
| v <- logData.get(k)
| r <- v.toOption
| } yield k -> r
res2: scala.collection.immutable.Set[(String, Int)] = Set((Metric_1,1), (Metric_2,0))

Playframework [Scala]: Binding a list/seq to the controller

I am trying to bind a list of input items on my html page to my controller.
My form is defined as:
def clientForm = Form( tuple(
"clients[]" -> seq( tuple(
"firstname" -> text,
"lastname" -> text) )
) )
In my HTML I have tried the following:
#b3.text(thisForm("clients[0]"), '_label -> "first client", 'value -> "('John','Snow')")
#b3.text(thisForm("clients[1]"), '_label -> "second client", 'value -> "('Frank','Carson')")
I have also tried:
#b3.text(thisForm("clients[0].firstname"), '_label -> "first client fistname", 'value -> "John")
#b3.text(thisForm("clients[0].lastname"), '_label -> "first client lastname", 'value -> "Snow")
#b3.text(thisForm("clients[1].firstname"), '_label -> "second client fistname", 'value -> "Frank")
#b3.text(thisForm("clients[2].lastname"), '_label -> "second client lastname", 'value -> "Carson")
In debug mode upon POST I can see that these values get bound to the form within the controller:
val boundFrom = inForm.bindFromRequest
But when I boundForm.fold mapping to clientForm the values to not map correctly to my "clients[]" element.
I'm at a loss and have spent ages looking for the answer to no avail.
Any help greatly appreciated
EDIT:
Here's a screenshot in debug mode. The values are being bound to the form but then do not get assigned.
Debug screenshot
Please note that your form definition can be as following -
def clientForm = Form(
single(
"clients[]" -> seq(
tuple(
"firstname" -> text,
"lastname" -> text
)
)
)
)
Then you second way of writing template is correct as you can see in documentation here https://www.playframework.com/documentation/2.6.x/ScalaForms#Repeated-values
Now what you will get when you fold this form is that a seq of tuples and you can use it like following -
clientForm.bindFromRequest.fold(
error => // Handle error
clients => {// this clients is of signature Seq[(String, String)]
clients.map{client => logger.log("First name: " + client._1 + ", Last name: " + client_2)}
}
)
Please note that you have to use tuple ._1 and ._2 to access your first name and last name here. As you are getting a tuple (String, String) not a Map[String, String] here.

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

JSON List to Scala List

I am creating a backend API with Play Framework and Scala. I would like to map the incoming request to a scala object. One of the instance variables of the object is a list of channels. Here is what I currently have:
Controller method that takes the request and attempts to map it to a user:
def addUser = Action(parse.json) { request =>
request.body.validate[User].fold({ errors =>
BadRequest(Json.obj(
"status" -> "Error",
"message" -> "Bad JSON",
"details" -> JsError.toFlatJson(errors)
))
}, { user =>
User.create(user.pushToken, user.channels)
Ok(Json.obj("status" -> "OK", "message" -> "User created"))
})
}
User case class:
case class User(id: Pk[Long], pushToken: String, channels: List[String])
User formatter:
implicit val userFormat = (
(__ \ "id").formatNullable[Long] and
(__ \ "pushToken").format[String] and
(__ \ "channels").format[List[String]]
)((id, pushToken, channels) => User(id.map(Id(_)).getOrElse(NotAssigned), pushToken, channels),
(u: User) => (u.id.toOption, u.pushToken, u.channels))
User anorm create method:
def create(pushToken: String, channels: List[String]) {
DB.withConnection { implicit c =>
SQL("insert into user (pushToken, channels) values ({pushToken}, {channels})").on(
'pushToken -> pushToken,
'channels -> channels
).executeUpdate()
}
}
When I try to compile, I get:
Compilation error[could not find implicit value for parameter extractor: anorm.Column[List[String]]]
Ideally, I would like to be able to accept this as a user:
{
"pushToken":"4jkf-fdsja93-fjdska34",
"channels": [
"channelA", "channelB", "channelC"
]
}
and create a user from it.
You can't use List[String] as column value in Anorm, thats the problem
You should use mkString method or smth else

Null values in JsObject for Option using play framework 2.1

I've got a case class with some optionals:
case class Person (
name: String,
nationality: Option[String],
email: Option[String],
gender: Option[String]
)
Using play 2.1.3 I'm trying to create a JSON looking like:
{"name": "Joe", "email": "john#doe.com"}
for an object:
val user = new User("Joe, None, Some("john#doe.com"), Some("male"))
with:
val myJson = Json.obj("name" -> user.name,
"nationality" -> user.nationality, "email" -> user.email)
I however get:
{"name": "Joe", "nationality": null, "email": "john#doe.com"}
How can I avoid the nationality with null value in the JSON?
After realizing that the problem was related to the play JSON handling, I managed to find a solution inspired by I need advice on Play's Json and elegant Option handling in the Writes trait. I'm not convinced that this is the most elegant solution out there, but it works:
def writes(person: Person): JsValue = {
JsObject(
Seq[(String, JsValue)]() ++
Some(person.name).map("name" -> JsString(_)) ++
person.email.map("email" -> JsString(_))
)
}