I've been recently migrating a playframework project from scala2 to scala3, and I found this error that only happens in scala 3 but it doesnt happen in scala 2:
play.sbt.PlayExceptions$CompilationException: Compilation error[Instance not found: 'Conversion[models.ErrorCode, _ <: Product]']
at play.sbt.PlayExceptions$CompilationException$.apply(PlayExceptions.scala:28)
at play.sbt.PlayExceptions$CompilationException$.apply(PlayExceptions.scala:28)
at scala.Option.map(Option.scala:230)
at play.sbt.run.PlayReload$.$anonfun$taskFailureHandler$8(PlayReload.scala:119)
at scala.Option.map(Option.scala:230)
at play.sbt.run.PlayReload$.taskFailureHandler(PlayReload.scala:92)
at play.sbt.run.PlayReload$.$anonfun$compile$3(PlayReload.scala:144)
at scala.util.Either$LeftProjection.map(Either.scala:573)
at play.sbt.run.PlayReload$.compile(PlayReload.scala:144)
at sbt.PlayRun$.$anonfun$playRunTask$5(PlayRun.scala:99)
The lines that are causing the error are this ones:
implicit val formatError: OFormat[ErrorCode] = Json.format[ErrorCode]
implicit val formatAccountException: OFormat[AccountException] = Json.format[AccountException]
Since it is an instance not found exception, I assume I need to use the new ErrorCode() instruction in some place of the code, but I have no idea where should it be since this code works perfectly in scala 2, and this error only happens in my scala 3 project. It's really confusing
I've been looking at the docs for play framework but I havent found anything even remotely related to the error. I have a light suspicion that I have to instanciate the "new ErrorCode" inside the controller that is having the conflict, and not in the class, but I might be wrong. I'll leave the code of the classes that are causing the error here:
The ErrorCode is just an Abstract class for creating a type that can be extended. I tried making it a case class and using the AccountException case classes as functions that returned it, but thats not really the behaviour I need in my application, I need it to return certain types.
abstract class ErrorCode {
val code: String = ""
val title: String = ""
val detail: Option[String] = None
}
//case class ErrorCode(code: String, title: String, detail: Option[String])
object ErrorCode {
def apply(codeValue: String, titleValue: String, detailValue: Option[String]): ErrorCode = new ErrorCode {
override val code: String = codeValue
override val title: String = titleValue
override val detail: Option[String] = detailValue
}
def unapply(err: ErrorCode): Option[(String, String, Option[String])] =
Some((err.code, err.title, err.detail))
}
This is the AccountException one, is a really long one:
abstract class AccountException(val errorCode: ErrorCode) {}
object AccountExceptions {
def apply(errorCode: ErrorCode): AccountException = new AccountException(errorCode) {}
def unapply(accEx: AccountException): Option[ErrorCode] = Some(accEx.errorCode)
case class AccountNotFoundException(resource: String) extends AccountException(AccountNotFound(resource))
case class AppNotFoundException(appKey: String) extends AccountException(AppNotFound(appKey))
case class AppTackConfigurationException(resource: String) extends AccountException(AppTackConfigurationError(resource))
case class InvalidCredentialsException(username: String) extends AccountException(InvalidCredentials(username))
case class MalformedAppleTokenException(userResource: String) extends AccountException(MalformedAppleToken(userResource))
case class MalformedGoogleTokenException(userResource: String) extends AccountException(MalformedGoogleToken(userResource))
case class ResourceNotFoundException(resource: String) extends AccountException(ResourceNotFound(resource))
case class UnknownException(username: String) extends AccountException(UnknownError(username))
case class UserAlreadyExistsException(username: String) extends AccountException(UserAlreadyExists(username))
case class UserNotFoundException(username: String) extends AccountException(UserNotFound(username))
}
object AccountErrorCodes {
case class AccountNotFound(resource: String) extends ErrorCode
{
override val code: String = "OAC07"
override val title: String = "Account not found"
override val detail: Option[String] = Some(resource)
}
case class AppNotFound(appKey: String) extends ErrorCode
{
override val code: String = "AME01"
override val title: String = "App not found"
override val detail: Option[String] = Some(appKey)
}
case class AppTackConfigurationError(configErrorDetail: String) extends ErrorCode
{
override val code: String = "AME07"
override val title: String = "Apptack configuration error"
override val detail: Option[String] = Some(configErrorDetail)
}
case class InvalidCredentials(username: String)extends ErrorCode
{
override val code: String = "OAC02"
override val title: String = "Credential invalid"
override val detail: Option[String] = Some(username)
}
case class MalformedAppleToken(userResource: String)extends ErrorCode
{
override val code: String = "OAC06"
override val title: String = "Malformed Apple sso token"
override val detail: Option[String] = Some(userResource)
}
case class MalformedGoogleToken(userResource: String) extends ErrorCode
{
override val code: String = "OAC06"
override val title: String = "Malformed google sso token"
override val detail: Option[String] = Some(userResource)
}
case class ResourceNotFound(resource: String) extends ErrorCode
{
override val code: String = "OAC07"
override val title: String = "Resource not found"
override val detail: Option[String] = Some(resource)
}
case class UnknownError(username: String) extends ErrorCode
{
override val code: String = "OAC05"
override val title: String = "Unexpected error"
override val detail: Option[String] = Some(username)
}
case class UserAlreadyExists(username: String) extends ErrorCode
{
override val code: String = "OAC03"
override val title: String = "User already exists"
override val detail: Option[String] = Some(username)
}
case class UserNotFound(username: String) extends ErrorCode
{
override val code: String = "OAC01"
override val title: String = "User not found"
override val detail: Option[String] = Some(username)
}
}
I could keep with the changing ErrorCode from an AbstractClass to a Case Class approach, and changing the AccountErrorCodes case classes for functions that return an ErrorCode, but Im using those specific types to manage the HTTP responses, so I would like to keep this behaviour:
//this function takes place in the controller
private def exceptionToResult(exception: AccountException): Result = {
exception match {
case e: AccountExceptions.AccountNotFoundException => NotFound(Json.toJson(exception))
case e: AccountExceptions.AppTackConfigurationException => BadRequest(Json.toJson(exception))
case e: AccountExceptions.InvalidCredentialsException => Unauthorized(Json.toJson(exception))
case e: AccountExceptions.MalformedAppleTokenException => BadRequest(Json.toJson(exception))
case e: AccountExceptions.MalformedGoogleTokenException => BadRequest(Json.toJson(exception))
case e: AccountExceptions.ResourceNotFoundException => NotFound(Json.toJson(exception))
case e: AccountExceptions.UnknownException => InternalServerError(Json.toJson(exception))
case e: AccountExceptions.UserAlreadyExistsException => Conflict(Json.toJson(exception))
case e: AccountExceptions.UserNotFoundException => NotFound(Json.toJson(exception))
case _ => InternalServerError(Json.toJson(exception))
}
}
This code compiles in scala 3 & play 3
import play.api.libs.json.{JsValue, Json, OWrites, Writes}
import play.api.mvc.Result
import play.api.mvc.Results.{InternalServerError, NotFound}
case class ErrorCode(code: String, title: String, detail: Option[String])
implicit val errorCodeWrites: OWrites[ErrorCode] = Json.writes[ErrorCode]
implicit val accountNotFoundExceptionWrites: OWrites[AccountException.AccountNotFoundException] =
Json.writes[AccountException.AccountNotFoundException]
implicit val appNotFoundExceptionWrites: OWrites[AccountException.AppNotFoundException] =
Json.writes[AccountException.AppNotFoundException]
//implicit val accountExceptionWrites: OWrites[AccountException] =
// Json.writes[AccountException]
implicit val accountExceptionWrites: Writes[AccountException] = new Writes[AccountException]:
def writes(o: AccountException): JsValue =
val w = implicitly[Writes[Option[String]]]
val builder = Json.newBuilder
builder += "code" -> o.errorCode.code
builder += "title" -> o.errorCode.title
builder += "detail" -> w.writes(o.errorCode.detail)
builder.result()
enum AccountException(val errorCode: ErrorCode) {
case AccountNotFoundException(resource: String)
extends AccountException(
ErrorCode(code = "OAC07", title = "Account Not Found", Some(resource))
)
case AppNotFoundException(appKey: String)
extends AccountException(
ErrorCode(code = "AME01", title = "App Not Found", Some(appKey))
)
}
def exceptionToResult(exception: AccountException): Result = {
exception match {
case AccountException.AccountNotFoundException(_) =>
NotFound(Json.toJson(exception))
// ....
case _ => InternalServerError(Json.toJson(exception))
}
}
I did not convert all your code and I didn't test it but I hope it helps. The code is based on my existing code from an app I wrote.