最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

playframework - How to write for play framework scala 3 enums reads, writes and format - Stack Overflow

programmeradmin7浏览0评论

I could see many threads pointing to solutions for Playframework json to work with scala 2 enums but none I could get working for scala 3 enums. Tried below solutions but not working

case class A(freq: Frequency) {
  def unapply(arg: A): Option[Frequency] = ???
  val form = Form(
    mapping("freq" -> of[Frequency])(A.apply)(A.unapply)
  )
}

enum Frequency derives EnumFormat {
  case None, Daily, Weekly   
  //implicit val format: Format[Frequency] = EnumFormat.derived[Frequency]
  //implicit val format: OFormat[A] = enumerationFormatter(Frequency.) //Json.format[A]
  //implicit val reads: Reads[Frequency] = Reads.enumNameReads(Frequency)
  //implicit val format: OFormat[Frequency] = Json.format[Frequency]
  //implicit val reads: Reads[Frequency] = Json.reads[Frequency]// //Json.toJson(this)
  implicit val format: OFormat[Frequency] = Json.formatEnum(this)
}
ERROR - Cannot find Formatter type class for models.Frequency. Perhaps you will need to import play.api.data.format.Formats._
on this line - mapping("freq" -> of[Frequency])(A.apply)(A.unapply)

EnumFormat from here - Also tried

How to bind an enum to a playframework form?

Worth to mention I am not a seasoned Scala programmer, still learning..

I could see many threads pointing to solutions for Playframework json to work with scala 2 enums but none I could get working for scala 3 enums. Tried below solutions but not working

case class A(freq: Frequency) {
  def unapply(arg: A): Option[Frequency] = ???
  val form = Form(
    mapping("freq" -> of[Frequency])(A.apply)(A.unapply)
  )
}

enum Frequency derives EnumFormat {
  case None, Daily, Weekly   
  //implicit val format: Format[Frequency] = EnumFormat.derived[Frequency]
  //implicit val format: OFormat[A] = enumerationFormatter(Frequency.) //Json.format[A]
  //implicit val reads: Reads[Frequency] = Reads.enumNameReads(Frequency)
  //implicit val format: OFormat[Frequency] = Json.format[Frequency]
  //implicit val reads: Reads[Frequency] = Json.reads[Frequency]// //Json.toJson(this)
  implicit val format: OFormat[Frequency] = Json.formatEnum(this)
}
ERROR - Cannot find Formatter type class for models.Frequency. Perhaps you will need to import play.api.data.format.Formats._
on this line - mapping("freq" -> of[Frequency])(A.apply)(A.unapply)

EnumFormat from here - https://github/playframework/play-json/issues/1017 Also tried

How to bind an enum to a playframework form?

Worth to mention I am not a seasoned Scala programmer, still learning..

Share Improve this question edited Mar 28 at 18:44 Gaël J 15.6k5 gold badges22 silver badges45 bronze badges asked Mar 27 at 9:46 Raj MalhotraRaj Malhotra 335 bronze badges 10
  • Have you tried to add the line import play.api.data.format.Formats._ as it is suggested in the error message? Have you tried the issue - play-json - Scala 3 enum support - workaround suggested by gbarmashiahflir? – Gastón Schabas Commented Mar 27 at 14:41
  • Yes this one only I have tried. As you see above enum Frequency does the same derives EnumFormat. It seems it works on resolving Json issue but form mapping does not work. – Raj Malhotra Commented Mar 27 at 15:28
  • I added complete code fro this link in a file, say EnumMacros. Further enum Frequency derives EnumFormat – Raj Malhotra Commented Mar 27 at 15:30
  • I have created a sample to see exact code here - github/hmalhotra20/scala3-enums-sample – Raj Malhotra Commented Mar 28 at 10:37
  • What is Formatter? That's not Play JSON. I wonder if you're not confusing JSON serialisation and the Form thing. – Gaël J Commented Mar 28 at 18:49
 |  Show 5 more comments

2 Answers 2

Reset to default 2

play-json is the lib to serialize/deserialize json, meanwhile play forms is for handling form submission.

To receive an Enum in a form, you need to implement a custom Formatter as it is detailed in the error message you got

ERROR - Cannot find Formatter type class for models.Frequency

This means, that the form mapping can't be done because it doesn't know how to parse Frequency. Something like this should fix it:

implicit val frequencyFormatter: Formatter[Frequency] = new Formatter[Frequency]:
    override def bind(key: String, data: Map[String, String]): Either[Seq[FormError], Frequency] =
      data
        .get(key) match
        // the value was passed in the form
        case Some(value) => Try(Frequency.valueOf(value)) match
          // if the value received is not a valid value of Frequency it will throw IllegalArgumentException
          case Failure(exception: IllegalArgumentException) => Left(Seq(FormError(key, "invalid value", value)))
          // if `valueOf` throws a different exception than `IllegalArgumentException`
          case Failure(exception) => Left(Seq(FormError(key, "some other error", value)))
          // if the value is valid
          case Success(frequency) => Right(frequency)
        // no `Frequency` was sent in the form
        case None => Left(Seq(FormError(key, "field frequency is empty")))

    override def unbind(key: String, value: Frequency): Map[String, String] = Map(key -> value.toString)

From play docs Play Forms - Custom binders for form mappings

Each form mapping uses an implicitly provided Formatter[T] binder object that performs the conversion of incoming String form data to/from the target data type.

case class UserCustomData(name: String, website: java.URL)
object UserCustomData {
  def unapply(u: UserCustomData): Option[(String, java.URL)] = 
    Some((u.name, > u.website))
}

To bind to a custom type like java.URL in the example above, define a form mapping like this:

val userFormCustom = Form(
  mapping(
    "name"    -> text,
    "website" -> of[URL]
  )(UserCustomData.apply)(UserCustomData.unapply)
)

For this to work you will need to make an implicit Formatter[java.URL] available to perform the data binding/unbinding.

import play.api.data.format.Formats._
import play.api.data.format.Formatter
implicit object UrlFormatter extends Formatter[URL] {
  override val format: Option[(String, Seq[Any])]           = Some(("format.url", Nil))
  override def bind(key: String, data: Map[String, String]) = parsing(new URL(_), "error.url", Nil)(key, data)
  override def unbind(key: String, value: URL)              = Map(key -> value.toString)
}

Note the Formats.parsing function is used to capture any exceptions thrown in the act of converting a String to target type T and registers a FormError on the form field binding.


Some time ago someone asked How to bind an enum to a playframework form?

But it was for scala 2 using Enumeration. Now in scala 3 there is a new way of having enums.

  • Alexandru Nedelcu - Scala 3 Enums

Thnx for helping in this. This might work I hv anyway switched to go, time being.

Additionally these solutions still look bit lengthy now as I hv 15+ such enums and many hv 12+ cases. This thread should help others preferably if some builtin shorter way comes to PLAY-SCALA later.

My initial guess was Play-form might be using Play-json internally as both are parsing same json request with header "Content-type:application/json". Otherwise if someone needs both this would do that twice.

I asked this to AI bots yestersday and below is answer by genimi

import play.api.libs.json._
import play.api.mvc._
import play.api.mvc.Results._
import scala.concurrent.{ExecutionContext, Future}
import javax.inject._

object PlayEnumHandling {

  // Define your Scala 3 Enum
  enum Platform {
    case WEB, MOBILE, DESKTOP, UNKNOWN
  }

  // Implicit JSON Formats for the Enum
  implicit val platformFormat: Format[Platform] = new Format[Platform] {
    def reads(json: JsValue): JsResult[Platform] = json match {
      case JsString(s) => s.toLowerCase() match {
        case "web" => JsSuccess(Platform.WEB)
        case "mobile" => JsSuccess(Platform.MOBILE)
        case "desktop" => JsSuccess(Platform.DESKTOP)
        case "unknown" => JsSuccess(Platform.UNKNOWN)
        case _ => JsError("Invalid platform string")
      }
      case _ => JsError("Expected JsString")
    }

    def writes(platform: Platform): JsValue = JsString(platform.toString.toLowerCase())
  }

  // Request DTO with the Enum
  case class RequestData(platform: Platform, data: String)

  implicit val requestDataFormat: Format[RequestData] = Json.format[RequestData]

  // Controller
  @Singleton
  class EnumController @Inject()(cc: ControllerComponents)(implicit ec: ExecutionContext) extends AbstractController(cc) {

    def handleRequest(): Action[JsValue] = Action.async(parse.json) { request =>
      request.body.validate[RequestData].fold(
        errors => Future.successful(BadRequest(Json.obj("status" -> "error", "message" -> JsError.toJson(errors)))),
        requestData => {
          Future.successful(Ok(Json.obj("status" -> "ok", "platform" -> requestData.platform, "data" -> requestData.data)))
        }
      )
    }
  }
}

It is clear either way thr would be good amount of extra code for each enum and then extra mapping json-read/write code for whole dto case classes too. Instead of writing less and elegant code, as scala promises, here my codebase would hv increased much bigger with non-functional code.

Below code is in go with same Gemini qstn:

package main

import (
        "net/http"
        "strings"

        "github/gin-gonic/gin"
)

// Platform Enum (using string constants for simplicity)
type Platform string

const (
        WEB     Platform = "web"
        MOBILE  Platform = "mobile"
        DESKTOP Platform = "desktop"
        UNKNOWN Platform = "unknown"
)

// Request Data Structure with validation tags.
type RequestData struct {
        Platform Platform `json:"platform" binding:"required"`
        Data     string   `json:"data" binding:"required"`
}

func main() {
        r := gin.Default()

        r.POST("/enum-request", func(c *gin.Context) {
                var requestData RequestData
                if err := c.ShouldBindJSON(&requestData); err != nil {
                        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
                        return
                }


                c.JSON(http.StatusOK, gin.H{
                        "status":   "ok",
                        "platform": requestData.Platform,
                        "data":     requestData.Data,
                })
        })

        r.Run(":8080") // Listen and serve on 0.0.0.0:8080
}

Added above example in case any help in comparison, at least for object modeling.

发布评论

评论列表(0)

  1. 暂无评论