JS client of rest api can send both int and string as a value of some field.
{
field1: "123",
field2: "456"
}
{
field1: 123,
field2: 456
}
Here is play action with case class to which json request body should be converted:
case class Dto(field1: Int, field2: Int)
object Dto {
implicit val reads = Json.reads[Dto]
}
def create = Action.async(BodyParsers.parse.json) { implicit request =>
request.body.validate[Dto].map {
dto => someService.doStuff(dto).map(result => Ok(Json.toJson(result)))
}.recoverTotal {
e => jsErrorToBadRequest(e)
}
}
In case if I send json values with int values, it works ok. But in case if field1 or field2 are strings ("123", "456"), it fails, because request.body.validate expects Int.
But problem is that JS client sends values from input fields, and input fields are converted to strings.
What is the best way to handle either ints or strings? (So this action should convert json to dto in both cases)
You could also define a more tolerant Reads[Int]
. And use it to define your Reads[Dto]
1) Define a more tolerant Reads[Int]
:
import play.api.data.validation.ValidationError
import play.api.libs.json._
import scala.util.{Success, Try}
// Define a more tolerant Reads[Int]
val readIntFromString: Reads[Int] = implicitly[Reads[String]]
.map(x => Try(x.toInt))
.collect (ValidationError(Seq("Parsing error"))){
case Success(a) => a
}
val readInt: Reads[Int] = implicitly[Reads[Int]].orElse(readIntFromString)
Examples:
readInt.reads(JsNumber(1))
// JsSuccess(1,)
readInt.reads(JsString("1"))
// JsSuccess(1,)
readInt.reads(JsString("1x"))
// JsError(List((,List(ValidationError(List(Parsing error),WrappedArray())))
2) Use your more tolerant Reads[Int]
to define your Reads[Dto]
:
implicit val DtoReads =
(JsPath \ "field1").read[Int](readInt) and
(JsPath \ "field2").read[Int](readInt)
EDIT: Differences with millhouse's solution:
if field1
is a string and field2
is an int with this solution you'll get a JsSuccess
but a JsError
with millhouse's solution
If both field are invalid with this solution you'll get a JsError
containing one error for each field. With millhouse's solution you'll get the first error.
You need a custom Reads
implementation for your Dto
- ie a Reads[Dto]
. I always like to start with the "built-in" (macro-generated) one you get via Json.reads[Dto]
- and then go from there; eg:
object Dto {
val basicReads = Json.reads[Dto]
implicit val typeCorrectingReads = new Reads[Dto]{
def reads(json: JsValue): JsResult[Dto] = {
def readAsInteger(fieldName:String):JsResult[Int] = {
(json \ fieldName).validate[String].flatMap { s =>
// We've got a String, but it might not be convertible to an int...
Try(s.toInt).map(JsSuccess(_)).getOrElse {
JsError(JsPath \ fieldName, s"Couldn't convert string $s to an integer")
}
}
}
basicReads.reads(json).orElse {
for {
f1 <- readAsInteger("field1")
f2 <- readAsInteger("field2")
} yield {
Dto(f1, f2)
}
}
}
}
}
By doing it this way, you get the basicReads
to do the work in the "happy case". If it doesn't work out, we then try treating the fields as String
instances, before finally attempting the conversion to an Int
.
Note how wherever possible, we're working inside the scope of a JsResult
that was created by "somebody else", so we'll fail fast.
It is actually quite easy with or
combinator (documented here https://www.playframework.com/documentation/2.6.x/ScalaJsonCombinators ) and little reads function.
case class Customer(name: String, number: Int)
object Customer {
val readIntFromString: Reads[Int] = implicitly[Reads[String]]
.map(x => x.toInt)
import play.api.libs.functional.syntax._
import play.api.libs.json._
implicit val routeReads: Reads[Customer] =
((__ \ "name").read[String] and
((__ \ "number").read[Int] or
(__ \ "number").read[Int](readIntFromString)))(Customer.apply _)
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.