play-json
play-json copied to clipboard
"no known subclasses" error when sealed trait sub-types are nested inside the companion object
Play JSON Version (2.5.x / etc)
2.9.0
API (Scala / Java / Neither / Both)
Tested with Scala 2.12.12
Expected Behavior
- I define a sealed trait.
- Inside the sealed trait's companion object, I define a case class implementing the trait.
- I define a
Format
for the sealed trait , using theJson.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
- 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
}
}
- 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
}
}
- 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)
}
}
I'll see if I can find out how to fix it and submit a pull request.
Tested with Scala 2.12.12
also on scala 2.11.12
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.
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)... 😞
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?
Move subclass outside companion
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.
I think it should support nested case object
s because usually companion object
acts as a namespace for enum variants.
Not possible, macro limitation. Unless Scala itself backports some fix. Nothing new can be done there.