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

53 Views Asked by At

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.

1

There are 1 best solutions below

0
Dmytro Mitin On BEST ANSWER

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).