简体   繁体   English

Play框架JSON读取:如何读取String或Int?

[英]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. rest api的JS客户端可以将int和string作为某个字段的值发送。

{
   field1: "123",
   field2: "456"
}

{
   field1: 123,
   field2: 456
}

Here is play action with case class to which json request body should be converted: 以下是应该转换json请求正文的case类的play动作:

  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. 如果我发送带有int值的json值,它可以正常工作。 But in case if field1 or field2 are strings ("123", "456"), it fails, because request.body.validate expects Int. 但是如果field1或field2是字符串(“123”,“456”),则它会失败,因为request.body.validate需要Int。

But problem is that JS client sends values from input fields, and input fields are converted to strings. 但问题是JS客户端从输入字段发送值,输入字段转换为字符串。

What is the best way to handle either ints or strings? 处理整数或字符串的最佳方法是什么? (So this action should convert json to dto in both cases) (所以这个动作应该在两种情况下都将json转换为dto)

You could also define a more tolerant Reads[Int] . 您还可以定义更宽容的Reads[Int] And use it to define your Reads[Dto] 并用它来定义你的Reads[Dto]

1) Define a more tolerant Reads[Int] : 1)定义更宽容的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] : 2)使用更宽容的Reads[Int]来定义Reads[Dto]

implicit val DtoReads = 
    (JsPath \ "field1").read[Int](readInt) and 
    (JsPath \ "field2").read[Int](readInt)

EDIT: Differences with millhouse's solution: 编辑:与millhouse的解决方案的差异:

  • 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 如果field1是一个字符串field2是与此解决方案,您会得到一个int JsSuccessJsError与米尔豪斯的解决方案

  • If both field are invalid with this solution you'll get a JsError containing one error for each field. 如果此解决方案的两个字段都无效,您将获得包含每个字段一个错误的JsError With millhouse's solution you'll get the first error. 使用millhouse的解决方案,您将收到第一个错误。

You need a custom Reads implementation for your Dto - ie a Reads[Dto] . 您需要为Dto实现自定义Reads实现 - 即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; 我总是喜欢从Json.reads[Dto]获得的“内置”(宏生成)开始 - 然后从那里开始; 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". 通过这种方式,您可以获得basicReads来完成“快乐案例”中的工作。 If it doesn't work out, we then try treating the fields as String instances, before finally attempting the conversion to an Int . 如果它不成功,我们会尝试将字段视为String实例,然后再尝试转换为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. 请注意, JsResult ,我们在由“其他人”创建的JsResult范围内工作,因此我们将快速失败。

It is actually quite easy with or combinator (documented here https://www.playframework.com/documentation/2.6.x/ScalaJsonCombinators ) and little reads function. 它实际上非常容易使用or组合(在此处记录https://www.playframework.com/documentation/2.6.x/ScalaJsonCombinators )和小读取功能。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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