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 | Show 5 more comments2 Answers
Reset to default 2play-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 typeT
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 enum
s.
- 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.
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:41Formatter
? That's not Play JSON. I wonder if you're not confusing JSON serialisation and theForm
thing. – Gaël J Commented Mar 28 at 18:49