简体   繁体   中英

Play framework JSON reads: How to read either String or Int?

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM