No instance of play.api.libs.json.Format is availa

2020-03-26 11:10发布

问题:

No instance of play.api.libs.json.Format is available for models.AccountStatus in the implicit scope.

This is the code taken from a github page, and only class names and variable names are changed.

package models

import slick.jdbc.H2Profile._
import play.api.libs.json._

case class Account(id: Long, name: String, category: Int, status:AccountStatus)

object Account {
  implicit val accountFormat = Json.format[Account]
}

sealed abstract class AccountStatus(val as:Int)

object AccountStatus{
  final case object Draft extends AccountStatus(0)
  final case object Active extends AccountStatus(1)
  final case object Blocked extends AccountStatus(2)
  final case object Defaulter extends AccountStatus(3)

  implicit val columnType: BaseColumnType[AccountStatus] = MappedColumnType.base[AccountStatus,Int](AccountStatus.toInt, AccountStatus.fromInt)

  private def toInt(as:AccountStatus):Int = as match {
    case Draft => 0
    case Active => 1
    case Blocked => 2
    case Defaulter => 3
  }

  private def fromInt(as: Int): AccountStatus = as match {
    case 0 => Draft
    case 1 => Active
    case 2 => Blocked
    case 3 => Defaulter
    _ => sys.error("Out of bound AccountStatus Value.")
  }
}

https://github.com/playframework/play-scala-slick-example/blob/2.6.x/app/models/Person.scala

回答1:

So, this code needs to be added inside of the object AccountStatus code block since we need to use fromInt to transform an Int to an AccountStatus. This is a Reads defined for AccountStatus:

implicit object AccountStatusReads extends Reads[AccountStatus] {
  def reads(jsValue: JsValue): JsResult[AccountStatus] = {
   (jsValue \ "as").validate[Int].map(fromInt)
  }
}

What's a Reads? It's just a trait that defines how a JsValue (the play class encapsulating JSON values) should be deserialized from JSON to some type. The trait only requires one method to be implemented, a reads method which takes in some json and returns a JsResult of some type. So you can see in the above code that we have a Reads that will look for a field in JSON called as and try to read it as an integer. From there, it will then transform it into an AccountStatus using the already defined fromInt method. So for example in the scala console you could do this:

import play.api.libs.json._ 
// import wherever account status is and the above reader
scala> Json.parse("""{"as":1}""").as[AccountStatus]
res0: AccountStatus = Active

This reader isn't perfect though, mainly because it's not handling the error your code will give you on out of bound numbers:

scala> Json.parse("""{"as":20}""").as[AccountStatus]
java.lang.RuntimeException: Out of bound AccountStatus Value.
  at scala.sys.package$.error(package.scala:27)
  at AccountStatus$.fromInt(<console>:42)
  at AccountStatusReads$$anonfun$reads$1.apply(<console>:27)
  at AccountStatusReads$$anonfun$reads$1.apply(<console>:27)
  at play.api.libs.json.JsResult$class.map(JsResult.scala:81)
  at play.api.libs.json.JsSuccess.map(JsResult.scala:9)
  at AccountStatusReads$.reads(<console>:27)
  at play.api.libs.json.JsValue$class.as(JsValue.scala:65)
  at play.api.libs.json.JsObject.as(JsValue.scala:166)
  ... 42 elided

You could handle this by making the Reads handle the error. I can show you how if you want, but first the other part of a Format is a Writes. This trait, unsurprisingly is similar to reads except it does the reverse. You're taking your class AccountStatus and creating a JsValue (JSON). So, you just have to implement the writes method.

implicit object AccountStatusWrites extends Writes[AccountStatus] {
  def writes(as: AccountStatus): JsValue = {
    JsObject(Seq("as" -> JsNumber(as.as)))
  }
}

Then this can be used to serialize that class to JSON like so:

scala> Json.toJson(Draft)
res4: play.api.libs.json.JsValue = {"as":0}

Now, this is actually enough to get your error to go away. Why? Because Json.format[Account] is doing all the work we just did for you! But for Account. It can do this because it's a case class and has less than 22 fields. Also every field for Account has a way to be converted to and from JSON (via a Reads and Writes). Your error message was showing that Account could not have a format automatically created for it because part of it (status field) had no formatter.

Now, why do you have to do this? Because AccountStatus is not a case class, so you can't call Json.format[AccountStatus] on it. And because the subclasses of it are each objects, which have no unapply method defined for them since they're not case classes. So you have to explain to the library how to serialize and deserialize.

Since you said you're new to scala, I imagine that the concept of an implicit is still somewhat foreign. I recommend you play around with it / do some reading to get a grasp of what to do when you see that the compiler is complaining about not being able to find an implicit it needs.

Bonus round

So, you might really not want to do that work yourself, and there is a way to avoid having to do it so you can do Json.format[AccountStatus]. You see Json.format uses the apply and unapply methods to do its dirty work. In scala, these two methods are defined automatically for case classes. But there's no reason you can't define them yourself and get everything they give you for free!

So, what do apply and unapply look like type signature wise? It changes per class, but in this case apply should match Int => AccountStatus (a function that goes from an int to an AccountStatus). So it's defined like so:

def apply(i: Int): AccountStatus = fromInt(i)

and unapply is similar to the reverse of this, but it needs to return an Option[Int], so it looks like

def unapply(as: AccountStatus): Option[Int] = Option(as.as)

with both of these defined you don't need to define the reads and writes yourself and instead can just call

// this is still inside the AccountStatus object { ... } 
implicit val asFormat = Json.format[AccountStatus]

and it will work in a similar fashion.

.P.S. I'm traveling today, but feel free to leave any comments if some of this doesn't make sense and I'll try to get back to you later on