You can google spray json derivation
https://github.com/driver-oss/spray-json-derivation
https://github.com/milessabin/spray-json-shapeless
https://github.com/zackangelo/spray-json-macros
https://github.com/ExNexu/spray-json-annotation
etc.
Since the signature of buildFormatter
is
def buildFormatter[T](str: String, value: T => String, apply: String => T): JsonFormat[T] = ???
(and you said that wrapper case classes are always single-parameter, parameter type is always String
, parameter name is always value
) you can try Shapeless
import shapeless.{::, Generic, HList, HNil, Typeable}
object caseClassJsonFormats {
implicit def caseClassJsonFormat[A <: Product, L <: HList](implicit
gen: Generic.Aux[A, String :: HNil],
typeable: Typeable[A]
): JsonFormat[A] =
buildFormatter[A](typeable.describe, gen.to(_).head, s => gen.from(s :: HNil))
}
So you define a single implicit for all case classes (instead of an implicit per each case class).
Testing:
final case class ClassName(value: String)
import caseClassJsonFormats._
implicitly[JsonFormat[ClassName]] // compiles
Alternative approach is a macro annotation (sbt settings for macro projects)
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
@compileTimeOnly("enable macro annotations")
class jsonFormat extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro JsonFormatMacro.impl
}
object JsonFormatMacro {
def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
import c.universe._
def jsonFormatImplicit(tpname: TypeName) =
q"""implicit val jsonFormatter: _root_.spray.json.JsonFormat[$tpname] =
buildFormatter[$tpname](${tpname.toString}, _.value, this.apply)"""
annottees match {
// if there is companion object, modify it
case (clazz@q"$_ class $tpname[..$_] $_(...$_) extends { ..$_ } with ..$_ { $_ => ..$_ }") ::
q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }" :: Nil =>
q"""
$clazz
$mods object $tname extends { ..$earlydefns } with ..$parents { $self =>
..$body
${jsonFormatImplicit(tpname)}
}"""
// if there is no companion object, create it
case (clazz@q"$_ class $tpname[..$_] $_(...$_) extends { ..$_ } with ..$_ { $_ => ..$_ }") :: Nil =>
q"""
$clazz
object ${tpname.toTermName} {
${jsonFormatImplicit(tpname)}
}"""
}
}
}
So you define an implicit in companion object per each case class annotated.
Testing:
@jsonFormat
final case class ClassName(value: String)
implicitly[JsonFormat[ClassName]] // compiles
// scalacOptions += "-Ymacro-debug-lite"
//scalac: {
// final case class ClassName extends scala.Product with scala.Serializable {
// <caseaccessor> <paramaccessor> val value: String = _;
// def <init>(value: String) = {
// super.<init>();
// ()
// }
// };
// object ClassName extends scala.AnyRef {
// def <init>() = {
// super.<init>();
// ()
// };
// implicit val jsonFormatter: _root_.spray.json.JsonFormat[ClassName] = buildFormatter[ClassName]("ClassName", ((x$1) => x$1.value), this.apply)
// };
// ()
//}