scalaplayframeworkscala-3play-json

Compilation Error: Instance not found: Conversion[models.Errorcode, _ <: Product] on Scala 3 Playframework


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))
        }
    }

Solution

  • 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.