play-json icon indicating copy to clipboard operation
play-json copied to clipboard

"no known subclasses" error when sealed trait sub-types are nested inside the companion object

Open guilgaly opened this issue 3 years ago • 7 comments

Play JSON Version (2.5.x / etc)

2.9.0

API (Scala / Java / Neither / Both)

Tested with Scala 2.12.12

Expected Behavior

  1. I define a sealed trait.
  2. Inside the sealed trait's companion object, I define a case class implementing the trait.
  3. I define a Format for the sealed trait , using the Json.format macro.

I expect Json.format to generate a format for the sealed trait, relying on a discriminator field.

Actual Behavior

The Json.format macro generates the following compilation error:

[error] /Users/gga/Documents/Workspaces/temp/tests-json/src/main/scala/tests/BaseTrait1.scala:13:16: Sealed trait tests.BaseTrait1 is not supported: no known subclasses
[error]     Json.format[BaseTrait1]
[error]                ^

Reproducible Test Case

  1. Example of failing code:
import play.api.libs.json._

sealed trait BaseTrait1

object BaseTrait1 {

  case class CaseClass1(foo: String) extends BaseTrait1

  implicit val format: OFormat[BaseTrait1] = {
    implicit val caseClassFormat: OFormat[CaseClass1] = Json.format[CaseClass1]
    Json.format[BaseTrait1] // DOES NOT COMPILE
  }
}
  1. If the case class is not nested in the sealed trait's companion object, it works as expected:
import play.api.libs.json._

sealed trait BaseTrait2

case class CaseClass2(foo: String) extends BaseTrait2

object BaseTrait2 {

  implicit val format: OFormat[BaseTrait2] = {
    implicit val caseClassFormat: OFormat[CaseClass2] = Json.format[CaseClass2]
    Json.format[BaseTrait2] // COMPILES
  }
}
  1. The following simplistic macro does succeed in finding CaseClass1 in the first example, unlike the play-json macro:
import scala.annotation.tailrec
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

object TestMacros {
  def listSubTypes[A]: List[String] = macro listSubTypesImpl[A]

  def listSubTypesImpl[A: c.WeakTypeTag](
      c: blackbox.Context
  ): c.Expr[List[String]] = {
    import c.universe._

    val symbol = weakTypeOf[A].typeSymbol // Let's just assume it's a sealed trait...

    val subTypes = allSubTypes(c)(symbol)

    val tree =
      q"List(${subTypes.map(t => s"${t.name.decodedName.toString}").mkString(", ")})"
    c.Expr[List[String]](tree)
  }

  private def allSubTypes(
      c: blackbox.Context
  )(symbol: c.Symbol): Set[c.Symbol] = {
    @tailrec
    def allSubClasses(
        path: Traversable[c.Symbol],
        subClasses: Set[c.Symbol]
    ): Set[c.Symbol] =
      path.headOption match {
        case Some(subSymbol) if subSymbol.isAbstract =>
          // Search subtypes
          allSubClasses(
            path.tail ++ subSymbol.asClass.knownDirectSubclasses,
            subClasses
          )
        case Some(subSymbol) =>
          allSubClasses(path.tail, subClasses ++ Set(subSymbol))
        case None =>
          subClasses
      }

    allSubClasses(symbol.asClass.knownDirectSubclasses, Set.empty)
  }
}

guilgaly avatar Aug 20 '20 15:08 guilgaly

I'll see if I can find out how to fix it and submit a pull request.

guilgaly avatar Aug 20 '20 15:08 guilgaly

Tested with Scala 2.12.12

also on scala 2.11.12

hochgi avatar Feb 27 '21 17:02 hochgi

I'm having a similar issue. https://stackoverflow.com/questions/66494266/sealed-trait-is-not-supported-play-json-2-9 This is using the code from a test.

dalmat36 avatar Mar 05 '21 14:03 dalmat36

Hi @hochgi and @dalmat36, FIY I discussed this with @cchantep and also tried a few things on my own a while ago, but didn't find a satisfying solution (the solutions I tried - based on using knownDirectSubclasses - introduced issues in some other situations, where the current macro code does work)... 😞

guilgaly avatar Apr 08 '21 14:04 guilgaly

im running into this issue now as well, but i doubt ill be able to fix it... are there any suggested workarounds or approaches? a fix on the horizon?

wim82 avatar Dec 09 '21 09:12 wim82

Move subclass outside companion

cchantep avatar Dec 09 '21 12:12 cchantep

thanks for the help @cchantep, though i should have pointed out that it really is very valuable to put the subclasses inside the companion object in this particular case :) I knew about that workaround, but it doesn't really help me. Having the subclasses in the companion object makes the instantiation a lot more logic when reading code, and of course the autocomplete is quite nice if you type SomeCompanionObject.

wim82 avatar Dec 15 '21 15:12 wim82

I think it should support nested case objects because usually companion object acts as a namespace for enum variants.

onsah avatar Mar 22 '23 12:03 onsah

Not possible, macro limitation. Unless Scala itself backports some fix. Nothing new can be done there.

cchantep avatar Mar 22 '23 13:03 cchantep