Search code examples
scalaplayframeworkscala-macrosplay-json

Generate case classes serializer and deserializer implicitly using play-json


I'm using play-json to map Json to case classes or enums. I'm looking for a smart way of creating Formats implicitly, since my project contains many types definitions.


At the moment I created a simple function to generate Formats for Enums:

def formatEnum[E <: Enumeration](enum: E) = Format(Reads.enumNameReads(enum), Writes.enumNameWrites)

But it takes a non-implicit argument so it cannot be used as implicit converter.


I tried to do the same for case classes:

implicit def caseFormat[A] = Json.format[A]

But I get the error "No unapply or unapplySeq function found", since Json.format is a macro which inspect the structure of the class.

Then I tried to create my macro in this way:

import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context

implicit def caseFormat[A](): Format[A] = macro impl[A]

def impl[A: c.WeakTypeTag](c: Context)(): c.Expr[Reads[A]] = {
    import c.universe._
    val TypeRef(pre, sym, args) = weakTypeTag[A].tpe
    val t = args.head
    val expr =  q"Json.format[$t]"
    c.Expr[Reads[A]](expr)
}

But the compiler does not find the implicit Format, though there is an implicit def that should generate the value.


Of course I can simply define many implicit val, but I think there is a smarter way to do it.


Solution

  • Assuming you have lots of case classes and you wish to json serialize it on the fly without having to write a play-json writer.

    import play.api.libs.json._
    import scala.reflect.runtime.{universe => ru}
    implicit class writeutil[T: ru.TypeTag](i: T) {
        implicit val writer = Json.writes[T]
    
        def toJson() = Json.toJson(i)
    }
    
    def toInstance[T: ru.TypeTag](s: String): Option[T] = {
        implicit val reader = Json.reads[T]
        Json.fromJson[T](Json.parse(s)) match {
            case JsSuccess(r: T, path: JsPath) => Option(r)
            case e: JsError => None
        }
    }
    

    An optimal implementation would be to re-use the reader/writer by caching and lookup. You can also read more about play-json.

    You can use this as:

    case class Entity(a: String, b: Int)
    val e = Entity("Stack", 0)
    
    e.toJson()