2
votes

Following this answer shows how to bind an Enumeration to a form using a case class.

However in Play 2.7.3 this code fails with:

No Json serializer found for type jura.SearchRequest. Try to implement an implicit Writes or Format for this type.

When I implement the formatter:

object SearchRequest {
  implicit val searchRequestFormat: OFormat[SearchRequest] = Json.format[SearchRequest]
}

I get

No instance of play.api.libs.json.Format is available for scala.Enumeration.Value in the implicit scope

Should I be trying to write a formatter for the system scala.Enumeration type?
Or is there another way to implement a formatter when Enumerations are involved?

Test case here.

3
Writes.enumNameWrites is provided. W/o the case class definition, I find weird to have scala.Enumeration.Value as property type, rather than E#Value (for some E <: scala.Enumeration)cchantep
Are you in control of SearchRequest and is it not used by other libraries?pme
@pme Yes, in control. It'll be used with Slick. Thanks!Jethro
Are you also interested in solutions replacing scala.Enumeration entirely?pme
@pme Yes. As I understand it the Jury's out on whether we should use the Enumeration class or just use case classes.. Any method which provides enum-like functionality would work - but the preference would be for the best-practice approach, which I took to mean using actual Enumerations.Jethro

3 Answers

2
votes

I use for any Enumeration this Library: enumeratum

With Dotty there will be great Enumerations, but until then I think moving to enumeratum is the best way handling Enumerations in Scala. See also Dotty - Enumerations.

As a bonus there is a play-json Extension, see Play JSON Extension.

With this your code would look like this:

import enumeratum.{ PlayJsonEnum, Enum, EnumEntry }

sealed trait SearchRequest extends EnumEntry

object SearchRequest extends Enum[SearchRequest] with PlayJsonEnum[SearchRequest] {

  val values = findValues

  case object SuperRequest  extends SearchRequest
  case object SimpleRequest extends SearchRequest
  ..
}

In essence PlayJsonEnum[SearchRequest] does all the work.

1
votes

To write the enum as a string as cchantep says you can use Writes.enumNameWrites, we specifically use to read and write the ID. Therefore we have an EnumFormat in the package global for enums:

package object enums {

  implicit def reads[E <: Enumeration](enum: E): Reads[E#Value] = new Reads[E#Value] {
    def reads(json: JsValue): JsResult[E#Value] = json match {
      case JsNumber(s) =>
        try {
          JsSuccess(enum.apply(s.toInt))
        } catch {
          case _: NoSuchElementException => JsError(s"Enumeration expected of type: '${enum.getClass}', but it does not appear to contain the value: '$s'")
        }
      case _ => JsError("Number value expected")
    }
  }

  implicit def writes[E <: Enumeration]: Writes[E#Value] = new Writes[E#Value] {
    def writes(v: E#Value): JsValue = JsNumber(v.id)
  }

  implicit def formatID[E <: Enumeration](enum: E): Format[E#Value] =
    Format(reads(enum), writes)


  def readsString[E <: Enumeration](enum: E): Reads[E#Value] = new Reads[E#Value] {
    def reads(json: JsValue): JsResult[E#Value] = json match {
      case JsString(s) => {
        try {
          JsSuccess(enum.withName(s))
        } catch {
          case _: NoSuchElementException => JsError(s"Enumeration expected of type: '${enum.getClass}', but it does not appear to contain the value: '$s'")
        }
      }
      case _ => JsError("String value expected")
    }
  }

  implicit def writesString[E <: Enumeration]: Writes[E#Value] = new Writes[E#Value] {
    def writes(v: E#Value): JsValue = JsString(v.toString)
  }

  implicit def formatString[E <: Enumeration](enum: E): Format[E#Value] =
    Format(readsString(enum), writesString)
}

And used:

object SearchRequest extends Enumeration(1) {
  type SearchRequest = Value

  val ONE /*1*/ , TWO /*2*/ , ETC /*n*/ = Value

  implicit val searchRequestFormat: Format[SearchRequest] = formatID(SearchRequest)
}
1
votes

Add the following to your Enumeration Object

implicit object MatchFilterTypeFormatter extends Formatter[MatchFilterType.Value] {
    override val format = Some(("format.enum", Nil))
    override def bind(key: String, data: Map[String, String]) = {
      try {
        Right(MatchFilterType.withName(data.get(key).head))
      } catch {
        case e:NoSuchElementException =>  Left(Seq(play.api.data.FormError(key, "Invalid MatchFilterType Enumeration")))
      }
    }
    override def unbind(key: String, value: MatchFilterType.Value) = {
      Map(key -> value.toString)
    }
  }
  implicit val matchFilterTypeFormat = new Format[MatchFilterType.MatchFilterType] {
    def reads(json: JsValue) = JsSuccess(MatchFilterType.withName(json.as[String]))
    def writes(myEnum: MatchFilterType.MatchFilterType) = JsString(myEnum.toString)
  }

And then the Formatter/Controller given in the question will work.

A working test case is here.