解析JSON正文时播放框架奇怪的行为

时间:2018-02-18 18:33:14

标签: scala http playframework header

我有一个接收JSON主体的端点。我有这种JSON格式的隐式读写。在端点中,我对JSON进行了验证并对结果进行了折叠!这是:

def updatePowerPlant(id: Int) = Action.async(parse.tolerantJson) { request =>
    request.body.validate[PowerPlantConfig].fold(
      errors => {
        Future.successful(
          BadRequest(
            Json.obj("message" -> s"invalid PowerPlantConfig ${errors.mkString(",")}")
          ).enableCors
        )
      },
      success => {
        dbService.insertOrUpdatePowerPlant(success).runAsync.materialize.map {
          case Failure(ex) =>
            InternalServerError(s"Error updating PowerPlant " +
              s"Reason => ${ex.getMessage}").enableCors
          case Success(result) =>
            result match {
              case Left(errorMessage) =>
                BadRequest(Json.obj("message" -> s"invalid PowerPlantConfig $errorMessage")).enableCors
              case Right(updatedConfig) =>
                Ok(Json.prettyPrint(Json.toJson(updatedConfig))).enableCors
            }
        }
      }
    )
  }

因为我可以看到我折叠错误并返回BadRequest。但是当我尝试编写单元测试时,我没有像我期望的那样将HTTP状态作为BadRequest,但是测试崩溃时出现如下异常:

JsResultException(errors:List((,List(ValidationError(List(error.expected.jsnumber),WrappedArray())))))
play.api.libs.json.JsResultException: JsResultException(errors:List((,List(ValidationError(List(error.expected.jsnumber),WrappedArray())))))
    at play.api.libs.json.JsReadable$$anonfun$2.apply(JsReadable.scala:23)
    at play.api.libs.json.JsReadable$$anonfun$2.apply(JsReadable.scala:23)
    at play.api.libs.json.JsResult$class.fold(JsResult.scala:73)
    at play.api.libs.json.JsError.fold(JsResult.scala:13)
    at play.api.libs.json.JsReadable$class.as(JsReadable.scala:21)
    at play.api.libs.json.JsDefined.as(JsLookup.scala:132)
    at com.inland24.plantsim.models.package$$anon$1.reads(package.scala:61)
    at play.api.libs.json.JsValue$class.validate(JsValue.scala:18)
    at play.api.libs.json.JsObject.validate(JsValue.scala:76)
    at com.inland24.plantsim.controllers.PowerPlantController$$anonfun$updatePowerPlant1$1.apply(PowerPlantController.scala:64)
    at com.inland24.plantsim.controllers.PowerPlantController$$anonfun$updatePowerPlant1$1.apply(PowerPlantController.scala:63)
    at play.api.mvc.Action$.invokeBlock(Action.scala:498)
    at play.api.mvc.Action$.invokeBlock(Action.scala:495)
    at play.api.mvc.ActionBuilder$$anon$2.apply(Action.scala:458)
    at com.inland24.plantsim.controllers.PowerPlantControllerTest$$anonfun$4$$anonfun$apply$mcV$sp$11.apply(PowerPlantControllerTest.scala:313)
    at com.inland24.plantsim.controllers.PowerPlantControllerTest$$anonfun$4$$anonfun$apply$mcV$sp$11.apply(PowerPlantControllerTest.scala:296)
    at org.scalatest.OutcomeOf$class.outcomeOf(OutcomeOf.scala:85)
    at org.scalatest.OutcomeOf$.outcomeOf(OutcomeOf.scala:104)
    at org.scalatest.Transformer.apply(Transformer.scala:22)
    at org.scalatest.Transformer.apply(Transformer.scala:20)
    at org.scalatest.WordSpecLike$$anon$1.apply(WordSpecLike.scala:1078)
    at org.scalatest.TestSuite$class.withFixture(TestSuite.scala:196)
    at com.inland24.plantsim.controllers.PowerPlantControllerTest.withFixture(PowerPlantControllerTest.scala:40)
    at org.scalatest.WordSpecLike$class.invokeWithFixture$1(WordSpecLike.scala:1075)
    at org.scalatest.WordSpecLike$$anonfun$runTest$1.apply(WordSpecLike.scala:1088)
    at org.scalatest.WordSpecLike$$anonfun$runTest$1.apply(WordSpecLike.scala:1088)
    at org.scalatest.SuperEngine.runTestImpl(Engine.scala:289)
    at org.scalatest.WordSpecLike$class.runTest(WordSpecLike.scala:1088)
    at com.inland24.plantsim.controllers.PowerPlantControllerTest.runTest(PowerPlantControllerTest.scala:40)
    at org.scalatest.WordSpecLike$$anonfun$runTests$1.apply(WordSpecLike.scala:1147)
    at org.scalatest.WordSpecLike$$anonfun$runTests$1.apply(WordSpecLike.scala:1147)
    at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:396)
    at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:384)
    at scala.collection.immutable.List.foreach(List.scala:392)
    at org.scalatest.SuperEngine.traverseSubNodes$1(Engine.scala:384)
    at org.scalatest.SuperEngine.org$scalatest$SuperEngine$$runTestsInBranch(Engine.scala:373)
    at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:410)
    at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:384)
    at scala.collection.immutable.List.foreach(List.scala:392)
    at org.scalatest.SuperEngine.traverseSubNodes$1(Engine.scala:384)
    at org.scalatest.SuperEngine.org$scalatest$SuperEngine$$runTestsInBranch(Engine.scala:379)
    at org.scalatest.SuperEngine.runTestsImpl(Engine.scala:461)
    at org.scalatest.WordSpecLike$class.runTests(WordSpecLike.scala:1147)
    at com.inland24.plantsim.controllers.PowerPlantControllerTest.runTests(PowerPlantControllerTest.scala:40)
    at org.scalatest.Suite$class.run(Suite.scala:1147)
    at com.inland24.plantsim.controllers.PowerPlantControllerTest.org$scalatest$WordSpecLike$$super$run(PowerPlantControllerTest.scala:40)
    at org.scalatest.WordSpecLike$$anonfun$run$1.apply(WordSpecLike.scala:1192)
    at org.scalatest.WordSpecLike$$anonfun$run$1.apply(WordSpecLike.scala:1192)
    at org.scalatest.SuperEngine.runImpl(Engine.scala:521)
    at org.scalatest.WordSpecLike$class.run(WordSpecLike.scala:1192)
    at com.inland24.plantsim.controllers.PowerPlantControllerTest.org$scalatest$BeforeAndAfterAll$$super$run(PowerPlantControllerTest.scala:40)
    at org.scalatest.BeforeAndAfterAll$class.liftedTree1$1(BeforeAndAfterAll.scala:213)
    at org.scalatest.BeforeAndAfterAll$class.run(BeforeAndAfterAll.scala:210)
    at com.inland24.plantsim.controllers.PowerPlantControllerTest.run(PowerPlantControllerTest.scala:40)
    at org.scalatest.tools.SuiteRunner.run(SuiteRunner.scala:45)
    at org.scalatest.tools.Runner$$anonfun$doRunRunRunDaDoRunRun$1.apply(Runner.scala:1340)
    at org.scalatest.tools.Runner$$anonfun$doRunRunRunDaDoRunRun$1.apply(Runner.scala:1334)
    at scala.collection.immutable.List.foreach(List.scala:392)
    at org.scalatest.tools.Runner$.doRunRunRunDaDoRunRun(Runner.scala:1334)
    at org.scalatest.tools.Runner$$anonfun$runOptionallyWithPassFailReporter$2.apply(Runner.scala:1011)
    at org.scalatest.tools.Runner$$anonfun$runOptionallyWithPassFailReporter$2.apply(Runner.scala:1010)
    at org.scalatest.tools.Runner$.withClassLoaderAndDispatchReporter(Runner.scala:1500)
    at org.scalatest.tools.Runner$.runOptionallyWithPassFailReporter(Runner.scala:1010)
    at org.scalatest.tools.Runner$.run(Runner.scala:850)
    at org.scalatest.tools.Runner.run(Runner.scala)
    at org.jetbrains.plugins.scala.testingSupport.scalaTest.ScalaTestRunner.runScalaTest2(ScalaTestRunner.java:138)
    at org.jetbrains.plugins.scala.testingSupport.scalaTest.ScalaTestRunner.main(ScalaTestRunner.java:28)

这是我的单元测试:

"not update for an invalid PowerPlantConfig JSON" in {
      // We are updating the PowerPlant with id = 101, Notice that the powerPlantId is invalid
      val jsBody =
        """
          |{
          |   "powerPlantId":"invalidId",
          |   "powerPlantName":"joesan 1",
          |   "minPower":100,
          |   "maxPower":800,
          |   "rampPowerRate":20.0,
          |   "rampRateInSeconds":"2 seconds",
          |   "powerPlantType":"RampUpType"
          |}
        """.stripMargin

      val result: Future[Result] =
        controller.updatePowerPlant(101)
          .apply(
            FakeRequest().withBody(Json.parse(jsBody))
          )
      result.materialize.map {
        case Success(succ) =>
          assert(succ.header.status === BAD_REQUEST)
        case Failure(_) =>
          fail("Unexpected test failure when Updating a PowerPlant! Please Analyze!")
      }
    }

知道为什么我没有得到预期的行为?我期待我收到HTTP BadRequest!

编辑:为了摆脱意外的异常,我不得不将我的代码包装到Try块中,我不希望这样。所以这段代码摆脱了错误:

def updatePowerPlant(id: Int) = Action.async(parse.tolerantJson) { request =>
    scala.util.Try(request.body.validate[PowerPlantConfig]) match {
      case Failure(fail) =>
        Future.successful(InternalServerError(s"Error updating PowerPlant " +
          s"Reason => ${fail.getMessage}").enableCors)
      case Success(succ) =>
        succ.fold(
          errors => {
            Future.successful(
              BadRequest(
                Json.obj("message" -> s"invalid PowerPlantConfig ${errors.mkString(",")}")
              ).enableCors
            )
          },
          success => {
            dbService.insertOrUpdatePowerPlant(success).runAsync.materialize.map {
              case Failure(ex) =>
                InternalServerError(s"Error updating PowerPlant " +
                  s"Reason => ${ex.getMessage}").enableCors
              case Success(result) =>
                result match {
                  case Left(errorMessage) =>
                    BadRequest(Json.obj("message" -> s"invalid PowerPlantConfig $errorMessage")).enableCors
                  case Right(updatedConfig) =>
                    Ok(Json.prettyPrint(Json.toJson(updatedConfig))).enableCors
                }
            }
          }
        )
    }
  }

但是可以看出还有这个额外的Try(....)块,我不想要这个!

以下是我对PowerPlantConfig的定义:

sealed trait PowerPlantConfig {
  def id: Int
  def name: String
  def minPower: Double
  def maxPower: Double
  def powerPlantType: PowerPlantType
}
object PowerPlantConfig {

  case class OnOffTypeConfig(
    id: Int,
    name: String,
    minPower: Double,
    maxPower: Double,
    powerPlantType: PowerPlantType
  ) extends PowerPlantConfig

  case class RampUpTypeConfig(
    id: Int,
    name: String,
    minPower: Double,
    maxPower: Double,
    rampPowerRate: Double,
    rampRateInSeconds: FiniteDuration,
    powerPlantType: PowerPlantType
  ) extends PowerPlantConfig

  case class UnknownConfig(
    id: Int = -1,
    name: String,
    minPower: Double,
    maxPower: Double,
    powerPlantType: PowerPlantType
  ) extends PowerPlantConfig

  // represents all the PowerPlant's from the database
  case class PowerPlantsConfig(
    snapshotDateTime: DateTime,
    powerPlantConfigSeq: Seq[PowerPlantConfig]
  )
}

这是我的JSON读写:

implicit val powerPlantCfgFormat: Format[PowerPlantConfig] = new Format[PowerPlantConfig] {
    def reads(json: JsValue): JsResult[PowerPlantConfig] = {
      val powerPlantTyp = PowerPlantType.fromString((json \ "powerPlantType").as[String])
      powerPlantTyp match {
        case PowerPlantType.OnOffType =>
         JsSuccess(OnOffTypeConfig(
            id = (json \ "powerPlantId").as[Int],
            name = (json \ "powerPlantName").as[String],
            minPower = (json \ "minPower").as[Double],
            maxPower = (json \ "maxPower").as[Double],
            powerPlantType = powerPlantTyp
          ))
        case PowerPlantType.RampUpType =>
          JsSuccess(RampUpTypeConfig(
            id = (json \ "powerPlantId").as[Int],
            name = (json \ "powerPlantName").as[String],
            minPower = (json \ "minPower").as[Double],
            rampPowerRate = (json \ "rampPowerRate").as[Double],
            rampRateInSeconds = Duration.apply((json \ "rampRateInSeconds").as[String]).asInstanceOf[FiniteDuration],
            maxPower = (json \ "maxPower").as[Double],
            powerPlantType = powerPlantTyp
          ))
        case _ =>
          JsSuccess(UnknownConfig(
            id = (json \ "powerPlantId").as[Int],
            name = (json \ "powerPlantName").as[String],
            minPower = (json \ "minPower").as[Double],
            maxPower = (json \ "maxPower").as[Double],
            powerPlantType = powerPlantTyp
          ))
      }
    }

    def writes(o: PowerPlantConfig): JsValue = {
      if (o.powerPlantType == RampUpType) {
        Json.obj(
          "powerPlantId" -> o.id,
          "powerPlantName" -> o.name,
          "minPower" -> o.minPower,
          "maxPower" -> o.maxPower,
          "rampPowerRate" -> o.asInstanceOf[RampUpTypeConfig].rampPowerRate,
          "rampRateInSeconds" -> o.asInstanceOf[RampUpTypeConfig].rampRateInSeconds.toString(),
          "powerPlantType" -> PowerPlantType.toString(o.powerPlantType)
        )
      }
      else {
        Json.obj(
          "powerPlantId" -> o.id,
          "powerPlantName" -> o.name,
          "minPower" -> o.minPower,
          "maxPower" -> o.maxPower,
          "powerPlantType" -> PowerPlantType.toString(o.powerPlantType)
        )
      }
    }
  }

1 个答案:

答案 0 :(得分:2)

根据您的堆栈跟踪(我标记的行)

JsResultException(errors:List((,List(ValidationError(List(error.expected.jsnumber),WrappedArray())))))
play.api.libs.json.JsResultException: JsResultException(errors:List((,List(ValidationError(List(error.expected.jsnumber),WrappedArray())))))
at play.api.libs.json.JsReadable$$anonfun$2.apply(JsReadable.scala:23)
at play.api.libs.json.JsReadable$$anonfun$2.apply(JsReadable.scala:23)
at play.api.libs.json.JsResult$class.fold(JsResult.scala:73)
at play.api.libs.json.JsError.fold(JsResult.scala:13)
--> at play.api.libs.json.JsReadable$class.as(JsReadable.scala:21)
at play.api.libs.json.JsDefined.as(JsLookup.scala:132)
at com.inland24.plantsim.models.package$$anon$1.reads(package.scala:61)
at play.api.libs.json.JsValue$class.validate(JsValue.scala:18)
at play.api.libs.json.JsObject.validate(JsValue.scala:76)
at com.inland24.plantsim.controllers.PowerPlantController$$anonfun$updatePowerPlant1$1.apply(PowerPlantController.scala:64)
at com.inland24.plantsim.controllers.PowerPlantController$$anonfun$updatePowerPlant1$1.apply(PowerPlantController.scala:63)

您使用as[Int]格式的Read id PowerPlantConfig字段as[Int]。 当您调用Int时,您试图强制给定的json路径键入as。如果它不能(如在你的测试中),它会引发异常。您可以阅读asOptvalidateas here之间的差异,例如

<强>更新

如果你研究asOptvalidatevalidate的实现,你会看到所有这三个一开始都做同样的事情,但在某种程度上有所不同:

reads - 我确实需要结果或失败的信息包装(只需在json上调用隐式arg的asOpt

as - 我需要结果或者没有结果,如果用于解决方案的读取返回解析错误,它将被忽略,因为根本没有设置

as - 我需要结果或异常。换句话说,&#34;我确定,如果没有,这总是这种类型,而不是一般错误&#34;

asOptas都是&#34;扩展验证&#34;解释结果。

示例

示例如何在层次结构中从validate移动到as(两种格式 - 一种用validate作为你的sealed trait PowerPlantConfig { def id: Int } case class RampUpTypeConfig(id: Int) extends PowerPlantConfig implicit val powerPlantCfgFormat: Format[PowerPlantConfig] = new Format[PowerPlantConfig] { def reads(json: JsValue): JsResult[PowerPlantConfig] = { JsSuccess(RampUpTypeConfig( id = (json \ "powerPlantId").as[Int] )) } def writes(o: PowerPlantConfig): JsValue = { Json.obj( "powerPlantId" -> o.id) } } val powerPlantCfgFormatFixed: Format[PowerPlantConfig] = new Format[PowerPlantConfig] { def reads(json: JsValue): JsResult[PowerPlantConfig] = { for { id <- (json \ "powerPlantId").validate[Int] } yield { RampUpTypeConfig( id = id ) } } def writes(o: PowerPlantConfig): JsValue = { Json.obj( "id" -> o.id) } } Json.parse("""{"powerPlantId":"123"}""").validate[PowerPlantConfig](powerPlantCfgFormatFixed) 将抛出异常,另一种用res1: play.api.libs.json.JsResult[PowerPlantConfig] = JsError(List((,List(ValidationError(error.expected.jsnumber,WrappedArray()))))) 不会抛出异常):

SELECT id,
        DATE_FORMAT(starttime, '%h:%i %p') AS starttime,
        DATE_FORMAT(endtime, '%h:%i %p') AS endtime
FROM TableName

输出不会是例外,但JsFailure是预期的

DATETIME