Search code examples
scalainheritanceimplicitplay-json

In Scala, Refer to Subclass in Abstract Super Class


Is there a way for the this keyword in a super class to refer to that class's subclass? Specifically, I am trying to do the following (the Json refers to Play's Json library):

abstract class A() {
  def toJson[T](implicit writes: Writes[T]): JsValue = Json.toJson(this)
}

case class B(myProperty: String) extends A
object B { implicit val bFormat = Json.format[B] }

This gives the error No Json serializer found for type A. Try to implement an implicit Writes or Format for this type.. So it's saying it can't serialize an object of type A, which makes sense. The goal, however, is for the this in Json.toJson(this) to refer to the subclass (which, in this instance, is B).

Is there any way to accomplish this? If not, is there any other way I can implement the Json.toJson(...) method in the superclass without having to implement in the subclass (and all other subclasses of A)?


Solution

  • The common trick to refer to the current subclass from the parent, is to use F-bounded polymorphism:

    // Here `T` refers to the type of the current subclass
    abstract class A[T <: A[T]]() { 
      this: T =>
      def toJson(implicit writes: Writes[T]): JsValue = Json.toJson(this)
    }
    
    // We have to specify the current subclass in `extends A[B]`
    case class B(myProperty: String) extends A[B]
    object B { implicit val bFormat = Json.format[B] }
    
    println(B("foo").toJson)
    

    This won't allow you to call toJson for any generic A though:

    val a: A[_] = B("foo")
    println(a.toJson)      // Doesn't compile with: 
                           //   No Json serializer found for type _$1. 
                           //   Try to implement an implicit Writes or Format for this type.
    

    To fix this you would have to save Writes for the subtype at the point of object creation:

    abstract class A[T <: A[T]](implicit writes: Writes[T]) { 
      this: T =>
      def toJson: JsValue = Json.toJson(this)
    }
    

    Or alternatively using the context bound notation:

    abstract class A[T <: A[T] : Writes] { 
      this: T =>
      def toJson: JsValue = Json.toJson(this)
    }
    

    And since this F-bounded polymorphism thing is just an implementation detail and always refering to a generic A as A[_] is quite boilerplate-y, you can move this code to an intermediate abstract class.

    So a full example looks like this:

    abstract class A() {
      def toJson: JsValue
    }
    
    abstract class AImpl[T <: AImpl[T] : Writes] extends A { 
      this: T =>
      def toJson: JsValue = Json.toJson(this)
    }
    
    case class B(myProperty: String) extends AImpl[B]
    object B { implicit val bFormat: Format[B] = Json.format[B] }
    
    val a: A = B("foo")
    println(a.toJson)