Search code examples
scalascala-macrosplay-json

exception during macro expansion: type T is not a class, play json


I'm facing this error:

exception during macro expansion: 
scala.ScalaReflectionException: type T is not a class
    at scala.reflect.api.Symbols$SymbolApi.asClass(Symbols.scala:284)
    at scala.reflect.api.Symbols$SymbolApi.asClass$(Symbols.scala:284)
    at scala.reflect.internal.Symbols$SymbolContextApiImpl.asClass(Symbols.scala:99)
    at play.api.libs.json.JsMacroImpl.directKnownSubclasses$1(JsMacroImpl.scala:527)
    at play.api.libs.json.JsMacroImpl.macroImpl(JsMacroImpl.scala:850)
    at play.api.libs.json.JsMacroImpl.implicitConfigWritesImpl(JsMacroImpl.scala:44)

When trying to declare a method to create the writer for a given generic type:

def makeWriter[T]: Writes[T] = Json.writes[T]

Now the implementation of the Json.writes is just a macro, and it doesn't require any sort of context bounds or something:

def writes[A]: OWrites[A] = macro JsMacroImpl.withOptionsWritesImpl[A]

Does anyone know what's going on here? I'm not really good at macros, so I'd appreciate if anyone could explain this, any solution is appreciated.


Solution

  • Yes, it's possible.

    The stack trace

    scala.ScalaReflectionException: type T is not a class
        at scala.reflect.api.Symbols$SymbolApi.asClass(Symbols.scala:284)
        at scala.reflect.api.Symbols$SymbolApi.asClass$(Symbols.scala:284)
        at scala.reflect.internal.Symbols$SymbolContextApiImpl.asClass(Symbols.scala:99)
        at play.api.libs.json.JsMacroImpl.directKnownSubclasses$1(JsMacroImpl.scala:555)
        at play.api.libs.json.JsMacroImpl.macroImpl(JsMacroImpl.scala:869)
        at play.api.libs.json.JsMacroImpl.implicitConfigWritesImpl(JsMacroImpl.scala:44)
    

    gives a hint what the problems is:

        def directKnownSubclasses: Option[List[Type]] = {
          // Workaround for SI-7046: https://issues.scala-lang.org/browse/SI-7046
          val tpeSym = atag.tpe.typeSymbol.asClass  //  <------
          ...
    

    https://github.com/playframework/play-json/blob/main/play-json/shared/src/main/scala-2/play/api/libs/json/JsMacroImpl.scala#L555

    .asClass can be called only on class symbols, otherwise it throws (and runtime exceptions of macros are compile errors of main code)

    def asClass: ClassSymbol = throw new ScalaReflectionException(s"$this is not a class")
    

    https://github.com/scala/scala/blob/2.13.x/src/reflect/scala/reflect/api/Symbols.scala#L284

    The problem of the definition

    def makeWriter[T]: Writes[T] = Json.writes[T]
    

    is that the macro Json.writes[T] is expanded here where T is not a class yet but just a type parameter. You can postpone macro expansion if you make your makeWriter a macro as well

    import play.api.libs.json.Writes
    import scala.language.experimental.macros
    import scala.reflect.macros.blackbox
    
    def makeWriter[T]: Writes[T] = macro makeWriterImpl[T]
    
    def makeWriterImpl[T: c.WeakTypeTag](c: blackbox.Context): c.Tree = {
      import c.universe._
      val typeT = weakTypeOf[T]
      q"_root_.play.api.libs.json.Json.writes[$typeT]"
    }
    
    // in a different subproject
    
    case class A(i: Int)
    
    makeWriter[A] // compiles
    

    Implicit Json Formatter for value classes in Scala

    In scala 2, can macro or any language feature be used to rewrite the abstract type reification mechanism in all subclasses? How about scala 3?

    Why the Scala compiler can provide implicit outside of object, but cannot inside? (answer)

    Type parameter for implicit valued method in Scala - Circe

    Parametric polymorphism issue with spark implicits, value toDF is not a member of Seq[T]

    Another solution to postpone macro expansion is to postpone implicit resolution (this postpones macro expansion because macros are now used to define implicits, i.e. instances of the type class Writes)

    def makeWriter[T: Writes]: Writes[T] = implicitly[Writes[T]]
    // aka
    //def makeWriter[T](implicit writes: Writes[T]): Writes[T] = writes
    

    See about the difference between implicitly[X] and (implicit x: X)

    When doing implicit resolution with type parameters, why does val placement matter?

    Setting abstract type based on typeclass

    and it doesn't require any sort of context bounds or something:

    You have correct suspection that context bounds (implicit parameters) can be hidden via macros (although now the reason of error was different):

    def foo()(implicit a: A) = ()
    

    can become

    def foo(): Unit = macro fooImpl
    
    def fooImpl(c: blackbox.Context)(): c.Tree = {
      import c.universe._
      c.inferImplicitValue(typeOf[A], silent = false)
      q"()"
    }
    

    (so def foo(): Unit now pretends that it doesn't require implicit A although actually it does).