Search code examples
scalaplayframeworkapplyclasscastexceptionunapply

What are the correct apply and unapply methods to avoid this java.lang.ClassCastException error in a Scala Play app?


I'm building a Scala Play app where events and data are persisted in Json format, and I'm trying to model users and the roles they're assigned. My thinking has been to model Roles as case objects, as each standard role only needs defining once for all users in the application, and I'd like to pattern match on the type of role a particular user has been assigned. So far, I have this;

package models

abstract class User {
  def displayName: String
  def role: Role
}

case class GuestUser(displayName: String, role: Role) extends User {
}

case class RegisteredUser(displayName: String, role: Role) extends User {
}

trait Role { // have tried abstract class too - but like idea of developing stackable traits for role permissions
}
object Role {
  implicit val RoleTypeFormat: Format[Role] = Json.format[Role]
  def apply(className: String): Role = Class.forName(className: String).asInstanceOf[Role]
  def unapply(role: Role): Option[String] = Option(this.getClass.getName) // have also tried .getSimpleName
}

case object GuestUserRole extends Role {
}

case object RegisteredUserRole extends Role {
}

If I don't define an apply and unapply method in object Role, and rely only on the implicit value that uses Json.format[Role], I get a 'no apply function found' or 'no unapply function found' error - so I added them, to try and get rid of this error.

I couldn't get it to compile without adding .asInstanceOf[Role] to the Role apply method. It now compiles, but when I try to set the role: Role parameter of a new RegisteredUser instance using,

val role: Role = RegisteredUserRole

a new RegisteredUser instance is created, where the role property gets serialized to Json as;

"role":{"className":"models.Role$”}

But when I try to deserialize it, I get Exception in thread "pool-4868-thread-1" java.lang.ClassCastException: java.lang.Class cannot be cast to models.Role

My aim is to end up with the same RegisteredUser instance (or GuestUser instance), so I can do pattern matching in the view controllers, along the lines of;

def isAuthorized: Boolean = { 
    role match { 
      case RegisteredUserRole => true
      case GuestUserRole => false
      // etc
    }
}

Any help and advice on this would be very much appreciated. I'm not yet skilled and knowledgeable enough in Scala and Play to know whether I'm even on the right track with modelling Users and Roles.


Solution

  • As @lmm suggested, it would be better to provide a custom Format[Role] rather than trying to create instances in a weird way.

    Something like this:

    implicit val fmt = new Format[Role] {
    
        def reads(js: JsValue): JsResult[Role] = {
            js.validate[String] fold (
                error => JsError(error),
                role => role match {
                    case "GuestUserRole" => JsSuccess(GuestUserRole)
                    case "RegisteredUserRole" => JsSuccess(RegisteredUserRole)
                    case _ => JsError(Nil) // Should probably contain some sort of `ValidationError`
                }
            )
        }
    
        def writes(r: Role): JsValue = JsString(r.toString)
    }