Search code examples
scalagenericsjson-deserializationscala-macrosupickle

Using upickle read in Scala 3 macro


Try to write a generic macro for deserialising case classes using uPickle read in Scala 3:

inline def parseJson[T:Type](x: Expr[String])(using Quotes): Either[String, Expr[T]] = '{
  try 
    Right(read[T]($x.toString))
  catch  
    case e: Throwable => Left(s"Deserialization error: ${e.getMessage}") 
}

get error:

  Right(read[T]($x.toString))
        ^^^^^^^^^^^^^^^^^^^^
missing argument for parameter evidence$3 of method read in trait Api: (implicit evidence$3: upickle.default.Reader[T]): T

I use the uPickle read for a bunch of case classes, is there a solution with less boilerplate code? Thanks for any help!


Solution

  • You need to:

    1. resolve the implicit and place the found value in Expr - you can expand a macro inside an inline def (just like vampyre macros from Scala 2), but NOT when you are constructing expressions with '{}, then you have to have all implicits resolved
    2. return Expr[Either[Throwable, T]] - when creating a body of a Scala 3 macro, the whole returned value has to be an Expr, otherwise you cannot unquote it within ${} (it might work as a value returned from an helper which would turn it into Expr, but ${} accepts only single call to something defined in top level scope)
    3. separate the body of Expr => Expr macro definition from its expansion - you cannot write "just" inline def something[T: Type](a: Expr[T])(using Quotes): Expr[T] = ... - body of a macro is a (non-inline) def taking Types and Exprs and Quotes and returning a single Expr. inline def has to unquote a single Expr with ${}... or just be a normal def which would be copy-pasted into the call site and resolved there. But then it unquotes no Exprs

    So it's either:

    // Quotes API requires separate inline def sth = ${ sthImpl }
    // and def sthImpl[T: Type](a: Expr[...])(using Quotes): Expr[...]
    
    inline def parseJson[T](x: String)(using r: Reader[T]): Either[Throwable, T] =
      ${ parseJsonImpl[T]('x)('r) }
    
    // Since it builds a complete TASTy, it cannot "defer" implicit
    // resolution to callsite. It either already gets value to put
    // into using, or has to import quotes.*, quotes.reflect.* and
    // then use Expr.summon[T].getOrElse(...)
    
    def parseJsonImpl[T:Type](
      x: Expr[String]
    )(
      reader: Expr[Reader[T]]
    )(using Quotes): Expr[Either[String, T]] = '{
      try 
        Right(read[T]($x)(using $reader))
      catch  
        case e: Throwable => Left(s"Deserialization error: ${e.getMessage}") 
    }
    

    or

    // inline def NOT using quotation API - NO Quotes, NO '{}
    
    inline def parseJson[T: Reader](x: String): Either[String, T] = {
      try 
        // Here, you can use something like a "vampyre macros" and 
        // let the compiler expand macros at the call site
        Right(read[T](x))
      catch  
        case e: Throwable => Left(s"Deserialization error: ${e.getMessage}") 
    }