Type parameter for implicit valued method in Scala - Circe
Asked Answered
C

1

2

I'm new to Scala, and using circe to model and serialize some API responses. I find myself using the following boilerplate

sealed trait SomeTrait

object SomeTrait {
    implicit val someEncoder: Encoder[SomeTrait] = deriveEncoder[SomeTrait]
    implicit val someDecoder: Decoder[SomeTrait] = deriveDecoder[SomeTrait]

    <code>
}

Instead, I would like to use generics, and define something like

trait SerializableTrait[A] {
    implicit val someEncoder: Encoder[A] = deriveEncoder[A]
    implicit val someDecoder: Decoder[A] = deriveDecoder[A]
}

And then just use extend it multiple times:

sealed trait SomeTrait

object SomeTrait extends SerializableTrait[SomeTrain] {

    <code>
}

But I'm getting could not find Lazy implicit value of type io.circe.generic.encoding.DerivedAsObjectEncoder and similarly for the decoder.

I know I might be trying to achieve circle.auto capabilities, but I want to understand what's wrong with this usage. Ideally I would want the compiler to evaluate the dervieEncoder/Decoder only when actually required, inside the non type-parameterized traits.

Chalfant answered 8/11, 2022 at 17:3 Comment(1)
If this could work, then either circe would provide it, or rather, we wouldn't need to derive anything at all. - Derivation happens at compile time and only works for some specific kind of case classes, that is why you have to call it.Droll
S
5

Your question is similar to Implicit Json Formatter for value classes in Scala . Just there json library was play-json and you're using Circe.

deriveEncoder and deriveDecoder are macros (actually not themselves but they call macros). So you can't call them where you want. deriveEncoder[A] and deriveDecoder[A] are trying to expand where A is not a case class or sealed trait (with case-class children) yet but an abstract type (a type parameter).

Extracting some common code to a trait and extending this trait is OOP way to avoid code duplication but macros is a different paradigm, namely metaprogramming. With metaprogramming you can not always follow OOP principles.

  • If you'd like to postpone expansion of the macros you can make your implicits implicit macros. This makes difference because in your code deriveEncoder[A] and deriveDecoder[A] were called (expanded) now for abstract A but with implicit macros they will be called (expanded) later when implicits are requested i.e. when A is inferred to be a concrete sealed trait or case class.
// in a different subproject

import io.circe.{Decoder, Encoder}
import scala.language.experimental.macros
import scala.reflect.macros.whitebox // libraryDependencies += scalaOrganization.value % "scala-reflect" % scalaVersion.value

trait SerializableTrait[A] {
  implicit def someEncoder: Encoder[A] = macro SerializableTraitMacros.someEncoderImpl[A]
  implicit def someDecoder: Decoder[A] = macro SerializableTraitMacros.someDecoderImpl[A]
}

class SerializableTraitMacros(val c: whitebox.Context) {
  import c.universe._

  val semiauto = q"_root_.io.circe.generic.semiauto"

  def someEncoderImpl[A: WeakTypeTag]: Tree = q"$semiauto.deriveEncoder[${weakTypeOf[A]}]"
  def someDecoderImpl[A: WeakTypeTag]: Tree = q"$semiauto.deriveDecoder[${weakTypeOf[A]}]"
}

Now you don't need to define implicits in companion objects of your case classes or sealed traits. You just need to make the companion objects extend SerializableTrait as you wanted

import io.circe.{Decoder, Encoder}

sealed trait SomeTrait

object SomeTrait extends SerializableTrait[SomeTrait]

case class A(i: Int, s: String) extends SomeTrait // the sealed trait must have at least one case-class child

implicitly[Encoder[SomeTrait]] // compiles
implicitly[Decoder[SomeTrait]] // compiles

With scalacOptions += "-Ymacro-debug-lite" (maybe also "-Xlog-implicits" can be usefull) you can see what codec instances Circe actually generates for the trait

//scalac: {
//  val inst$macro$14: io.circe.generic.encoding.DerivedAsObjectEncoder[App.SomeTrait] = {
//    final class anon$someEncoder$macro$13 extends _root_.scala.Serializable {
//      def <init>() = {
//        super.<init>();
//        ()
//      };
//      lazy val inst$macro$1: io.circe.generic.encoding.DerivedAsObjectEncoder[App.SomeTrait] = encoding.this.DerivedAsObjectEncoder.deriveEncoder[App.SomeTrait, shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out](shapeless.this.LabelledGeneric.materializeCoproduct[App.SomeTrait, (Symbol @@ String("A")) :: shapeless.HNil, App.A :+: shapeless.CNil, shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out](DefaultSymbolicLabelling.instance[App.SomeTrait, (Symbol @@ String("A")) :: shapeless.HNil](::.apply[Symbol @@ String("A"), shapeless.HNil.type](scala.Symbol.apply("A").asInstanceOf[Symbol @@ String("A")], HNil)), Generic.instance[App.SomeTrait, App.A :+: shapeless.CNil](((p: App.SomeTrait) => Coproduct.unsafeMkCoproduct((p: p: @_root_.scala.unchecked) match {
//  case (_: App.A) => 0
//}, p).asInstanceOf[App.A :+: shapeless.CNil]), ((x$1: App.A :+: shapeless.CNil) => Coproduct.unsafeGet(x$1).asInstanceOf[App.SomeTrait])), coproduct.this.ZipWithKeys.cpZipWithKeys[Symbol @@ String("A"), App.A, shapeless.HNil, shapeless.CNil](coproduct.this.ZipWithKeys.cnilZipWithKeys, Witness.mkWitness[Symbol with shapeless.tag.Tagged[String("A")]](scala.Symbol.apply("A").asInstanceOf[Symbol @@ String("A")].asInstanceOf[Symbol with shapeless.tag.Tagged[String("A")]])), scala.this.<:<.refl[shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out]), shapeless.Lazy.apply[io.circe.generic.encoding.ReprAsObjectEncoder[shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out]](inst$macro$2)).asInstanceOf[io.circe.generic.encoding.DerivedAsObjectEncoder[App.SomeTrait]];
//      lazy val inst$macro$2: io.circe.generic.encoding.ReprAsObjectEncoder[shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out] = ({
//  final class $anon extends io.circe.generic.encoding.ReprAsObjectEncoder[shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out] {
//    def <init>() = {
//      super.<init>();
//      ()
//    };
//    private[this] val circeGenericEncoderForA = shapeless.Lazy.apply[io.circe.generic.encoding.DerivedAsObjectEncoder[App.A]](inst$macro$3).value;
//    final def encodeObject(a: shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out): io.circe.JsonObject = shapeless.Inr.apply[Nothing, shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out](a) match {
//      case shapeless.Inr((circeGenericInrBindingForA @ _)) => circeGenericInrBindingForA match {
//        case shapeless.Inl((circeGenericInlBindingForA @ _)) => io.circe.JsonObject.singleton("A", $anon.this.circeGenericEncoderForA.apply(circeGenericInlBindingForA))
//        case shapeless.Inr(_) => scala.sys.`package`.error("Cannot encode CNil")
//      }
//    }
//  };
//  new $anon()
//}: io.circe.generic.encoding.ReprAsObjectEncoder[shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out]).asInstanceOf[io.circe.generic.encoding.ReprAsObjectEncoder[shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out]];
//      lazy val inst$macro$3: io.circe.generic.encoding.DerivedAsObjectEncoder[App.A] = encoding.this.DerivedAsObjectEncoder.deriveEncoder[App.A, shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out](shapeless.this.LabelledGeneric.materializeProduct[App.A, (Symbol @@ String("i")) :: (Symbol @@ String("s")) :: shapeless.HNil, Int :: String :: shapeless.HNil, shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out](DefaultSymbolicLabelling.instance[App.A, (Symbol @@ String("i")) :: (Symbol @@ String("s")) :: shapeless.HNil](::.apply[Symbol @@ String("i"), (Symbol @@ String("s")) :: shapeless.HNil.type](scala.Symbol.apply("i").asInstanceOf[Symbol @@ String("i")], ::.apply[Symbol @@ String("s"), shapeless.HNil.type](scala.Symbol.apply("s").asInstanceOf[Symbol @@ String("s")], HNil))), Generic.instance[App.A, Int :: String :: shapeless.HNil](((x0$3: App.A) => x0$3 match {
//  case App.this.A((i$macro$10 @ _), (s$macro$11 @ _)) => ::.apply[Int, String :: shapeless.HNil.type](i$macro$10, ::.apply[String, shapeless.HNil.type](s$macro$11, HNil)).asInstanceOf[Int :: String :: shapeless.HNil]
//}), ((x0$4: Int :: String :: shapeless.HNil) => x0$4 match {
//  case ::((i$macro$8 @ _), ::((s$macro$9 @ _), HNil)) => App.this.A.apply(i$macro$8, s$macro$9)
//})), hlist.this.ZipWithKeys.hconsZipWithKeys[Symbol @@ String("i"), Int, (Symbol @@ String("s")) :: shapeless.HNil, String :: shapeless.HNil, shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out](hlist.this.ZipWithKeys.hconsZipWithKeys[Symbol @@ String("s"), String, shapeless.HNil, shapeless.HNil, shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out](hlist.this.ZipWithKeys.hnilZipWithKeys, Witness.mkWitness[Symbol with shapeless.tag.Tagged[String("s")]](scala.Symbol.apply("s").asInstanceOf[Symbol @@ String("s")].asInstanceOf[Symbol with shapeless.tag.Tagged[String("s")]])), Witness.mkWitness[Symbol with shapeless.tag.Tagged[String("i")]](scala.Symbol.apply("i").asInstanceOf[Symbol @@ String("i")].asInstanceOf[Symbol with shapeless.tag.Tagged[String("i")]])), scala.this.<:<.refl[shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out]), shapeless.Lazy.apply[io.circe.generic.encoding.ReprAsObjectEncoder[shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out]](inst$macro$12)).asInstanceOf[io.circe.generic.encoding.DerivedAsObjectEncoder[App.A]];
//      lazy val inst$macro$12: io.circe.generic.encoding.ReprAsObjectEncoder[shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out] = ({
//  final class $anon extends io.circe.generic.encoding.ReprAsObjectEncoder[shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out] {
//    def <init>() = {
//      super.<init>();
//      ()
//    };
//    private[this] val circeGenericEncoderFori = circe.this.Encoder.encodeInt;
//    private[this] val circeGenericEncoderFors = circe.this.Encoder.encodeString;
//    final def encodeObject(a: shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out): io.circe.JsonObject = a match {
//      case shapeless.::((circeGenericHListBindingFori @ _), shapeless.::((circeGenericHListBindingFors @ _), shapeless.HNil)) => io.circe.JsonObject.fromIterable(scala.collection.immutable.Vector.apply[(String, io.circe.Json)](scala.Tuple2.apply[String, io.circe.Json]("i", $anon.this.circeGenericEncoderFori.apply(circeGenericHListBindingFori)), scala.Tuple2.apply[String, io.circe.Json]("s", $anon.this.circeGenericEncoderFors.apply(circeGenericHListBindingFors))))
//    }
//  };
//  new $anon()
//}: io.circe.generic.encoding.ReprAsObjectEncoder[shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out]).asInstanceOf[io.circe.generic.encoding.ReprAsObjectEncoder[shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out]]
//    };
//    new anon$someEncoder$macro$13().inst$macro$1
//  };
//  _root_.shapeless.Lazy.apply[io.circe.generic.encoding.DerivedAsObjectEncoder[App.SomeTrait]](inst$macro$14)
//}

//scalac: {
//  val inst$macro$14: io.circe.generic.decoding.DerivedDecoder[App.SomeTrait] = {
//    final class anon$someDecoder$macro$13 extends _root_.scala.Serializable {
//      def <init>() = {
//        super.<init>();
//        ()
//      };
//      lazy val inst$macro$1: io.circe.generic.decoding.DerivedDecoder[App.SomeTrait] = decoding.this.DerivedDecoder.deriveDecoder[App.SomeTrait, shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out](shapeless.this.LabelledGeneric.materializeCoproduct[App.SomeTrait, (Symbol @@ String("A")) :: shapeless.HNil, App.A :+: shapeless.CNil, shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out](DefaultSymbolicLabelling.instance[App.SomeTrait, (Symbol @@ String("A")) :: shapeless.HNil](::.apply[Symbol @@ String("A"), shapeless.HNil.type](scala.Symbol.apply("A").asInstanceOf[Symbol @@ String("A")], HNil)), Generic.instance[App.SomeTrait, App.A :+: shapeless.CNil](((p: App.SomeTrait) => Coproduct.unsafeMkCoproduct((p: p: @_root_.scala.unchecked) match {
//  case (_: App.A) => 0
//}, p).asInstanceOf[App.A :+: shapeless.CNil]), ((x$1: App.A :+: shapeless.CNil) => Coproduct.unsafeGet(x$1).asInstanceOf[App.SomeTrait])), coproduct.this.ZipWithKeys.cpZipWithKeys[Symbol @@ String("A"), App.A, shapeless.HNil, shapeless.CNil](coproduct.this.ZipWithKeys.cnilZipWithKeys, Witness.mkWitness[Symbol with shapeless.tag.Tagged[String("A")]](scala.Symbol.apply("A").asInstanceOf[Symbol @@ String("A")].asInstanceOf[Symbol with shapeless.tag.Tagged[String("A")]])), scala.this.<:<.refl[shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out]), shapeless.Lazy.apply[io.circe.generic.decoding.ReprDecoder[shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out]](inst$macro$2)).asInstanceOf[io.circe.generic.decoding.DerivedDecoder[App.SomeTrait]];
//      lazy val inst$macro$2: io.circe.generic.decoding.ReprDecoder[shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out] = ({
//  final class $anon extends io.circe.generic.decoding.ReprDecoder[shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out] {
//    def <init>() = {
//      super.<init>();
//      ()
//    };
//    private[this] val circeGenericDecoderForA = shapeless.Lazy.apply[io.circe.generic.decoding.DerivedDecoder[App.A]](inst$macro$3).value;
//    final def apply(c: io.circe.HCursor): io.circe.Decoder.Result[shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out] = {
//      val result = c.downField("A");
//      if (result.succeeded)
//        scala.Some.apply[io.circe.Decoder.Result[App.A]]($anon.this.circeGenericDecoderForA.tryDecode(result))
//      else
//        scala.None
//    } match {
//      case scala.Some((result @ _)) => result match {
//        case scala.util.Right((v @ _)) => scala.util.Right.apply[Nothing, shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out](ReprDecoder.injectLeftValue[Symbol @@ String("A"), App.A, shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out](v))
//        case scala.util.Left((err @ _)) => scala.util.Left.apply[io.circe.DecodingFailure, Nothing](err)
//      }
//      case scala.None => (scala.util.Left.apply[io.circe.DecodingFailure, shapeless.CNil](io.circe.DecodingFailure.apply("JSON decoding to CNil should never happen", c.history)): scala.util.Either[io.circe.DecodingFailure, shapeless.CNil]) match {
//        case scala.util.Right((v @ _)) => scala.util.Right.apply[Nothing, shapeless.Inr[Nothing,shapeless.CNil]](shapeless.Inr.apply[Nothing, shapeless.CNil](v))
//        case scala.util.Left((err @ _)) => scala.util.Left.apply[io.circe.DecodingFailure, Nothing](err)
//      }
//    };
//    final override def decodeAccumulating(c: io.circe.HCursor): io.circe.Decoder.AccumulatingResult[shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out] = {
//      val result = c.downField("A");
//      if (result.succeeded)
//        scala.Some.apply[io.circe.Decoder.AccumulatingResult[App.A]]($anon.this.circeGenericDecoderForA.tryDecodeAccumulating(result))
//      else
//        scala.None
//    } match {
//      case scala.Some((result @ _)) => result.map[shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out](((v: App.A) => ReprDecoder.injectLeftValue[Symbol @@ String("A"), App.A, shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out](v)))
//      case scala.None => cats.data.Validated.invalidNel[io.circe.DecodingFailure, shapeless.CNil](io.circe.DecodingFailure.apply("JSON decoding to CNil should never happen", c.history)).map[shapeless.Inr[Nothing,shapeless.CNil]](((x$3: shapeless.CNil) => shapeless.Inr.apply[Nothing, shapeless.CNil](x$3)))
//    }
//  };
//  new $anon()
//}: io.circe.generic.decoding.ReprDecoder[shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out]).asInstanceOf[io.circe.generic.decoding.ReprDecoder[shapeless.labelled.FieldType[Symbol @@ String("A"),App.A] :+: shapeless.ops.coproduct.ZipWithKeys.cnilZipWithKeys.Out]];
//      lazy val inst$macro$3: io.circe.generic.decoding.DerivedDecoder[App.A] = decoding.this.DerivedDecoder.deriveDecoder[App.A, shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out](shapeless.this.LabelledGeneric.materializeProduct[App.A, (Symbol @@ String("i")) :: (Symbol @@ String("s")) :: shapeless.HNil, Int :: String :: shapeless.HNil, shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out](DefaultSymbolicLabelling.instance[App.A, (Symbol @@ String("i")) :: (Symbol @@ String("s")) :: shapeless.HNil](::.apply[Symbol @@ String("i"), (Symbol @@ String("s")) :: shapeless.HNil.type](scala.Symbol.apply("i").asInstanceOf[Symbol @@ String("i")], ::.apply[Symbol @@ String("s"), shapeless.HNil.type](scala.Symbol.apply("s").asInstanceOf[Symbol @@ String("s")], HNil))), Generic.instance[App.A, Int :: String :: shapeless.HNil](((x0$3: App.A) => x0$3 match {
//  case App.this.A((i$macro$10 @ _), (s$macro$11 @ _)) => ::.apply[Int, String :: shapeless.HNil.type](i$macro$10, ::.apply[String, shapeless.HNil.type](s$macro$11, HNil)).asInstanceOf[Int :: String :: shapeless.HNil]
//}), ((x0$4: Int :: String :: shapeless.HNil) => x0$4 match {
//  case ::((i$macro$8 @ _), ::((s$macro$9 @ _), HNil)) => App.this.A.apply(i$macro$8, s$macro$9)
//})), hlist.this.ZipWithKeys.hconsZipWithKeys[Symbol @@ String("i"), Int, (Symbol @@ String("s")) :: shapeless.HNil, String :: shapeless.HNil, shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out](hlist.this.ZipWithKeys.hconsZipWithKeys[Symbol @@ String("s"), String, shapeless.HNil, shapeless.HNil, shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out](hlist.this.ZipWithKeys.hnilZipWithKeys, Witness.mkWitness[Symbol with shapeless.tag.Tagged[String("s")]](scala.Symbol.apply("s").asInstanceOf[Symbol @@ String("s")].asInstanceOf[Symbol with shapeless.tag.Tagged[String("s")]])), Witness.mkWitness[Symbol with shapeless.tag.Tagged[String("i")]](scala.Symbol.apply("i").asInstanceOf[Symbol @@ String("i")].asInstanceOf[Symbol with shapeless.tag.Tagged[String("i")]])), scala.this.<:<.refl[shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out]), shapeless.Lazy.apply[io.circe.generic.decoding.ReprDecoder[shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out]](inst$macro$12)).asInstanceOf[io.circe.generic.decoding.DerivedDecoder[App.A]];
//      lazy val inst$macro$12: io.circe.generic.decoding.ReprDecoder[shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out] = ({
//  final class $anon extends io.circe.generic.decoding.ReprDecoder[shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out] {
//    def <init>() = {
//      super.<init>();
//      ()
//    };
//    private[this] val circeGenericDecoderFori = circe.this.Decoder.decodeInt;
//    private[this] val circeGenericDecoderFors = circe.this.Decoder.decodeString;
//    final def apply(c: io.circe.HCursor): io.circe.Decoder.Result[shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out] = ReprDecoder.consResults[io.circe.Decoder.Result, Symbol @@ String("i"), Int, shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out]($anon.this.circeGenericDecoderFori.tryDecode(c.downField("i")), ReprDecoder.consResults[io.circe.Decoder.Result, Symbol @@ String("s"), String, shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out]($anon.this.circeGenericDecoderFors.tryDecode(c.downField("s")), ReprDecoder.hnilResult)(io.circe.Decoder.resultInstance))(io.circe.Decoder.resultInstance);
//    final override def decodeAccumulating(c: io.circe.HCursor): io.circe.Decoder.AccumulatingResult[shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out] = ReprDecoder.consResults[io.circe.Decoder.AccumulatingResult, Symbol @@ String("i"), Int, shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out]($anon.this.circeGenericDecoderFori.tryDecodeAccumulating(c.downField("i")), ReprDecoder.consResults[io.circe.Decoder.AccumulatingResult, Symbol @@ String("s"), String, shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out]($anon.this.circeGenericDecoderFors.tryDecodeAccumulating(c.downField("s")), ReprDecoder.hnilResultAccumulating)(io.circe.Decoder.accumulatingResultInstance))(io.circe.Decoder.accumulatingResultInstance)
//  };
//  new $anon()
//}: io.circe.generic.decoding.ReprDecoder[shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out]).asInstanceOf[io.circe.generic.decoding.ReprDecoder[shapeless.labelled.FieldType[Symbol @@ String("i"),Int] :: shapeless.labelled.FieldType[Symbol @@ String("s"),String] :: shapeless.ops.hlist.ZipWithKeys.hnilZipWithKeys.Out]]
//    };
//    new anon$someDecoder$macro$13().inst$macro$1
//  };
//  _root_.shapeless.Lazy.apply[io.circe.generic.decoding.DerivedDecoder[App.SomeTrait]](inst$macro$14)
//}

If you want implicitly[Encoder[A]], implicitly[Decoder[A]] to compile too you should define companion object A extends SerializableTrait[A]. So it's better to call SerializableTrait differently, SerializableTraitOrClass or something like that.

  • Alternatively, you can define macro annotation for classes/traits or their companion objects. It will generate necessary implicits inside companion objects
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.reflect.macros.blackbox
import scala.language.experimental.macros

@compileTimeOnly("""enable macro annotations: scalacOptions += "-Ymacro-annotations" """)
class serializableTrait extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro SerializableTraitMacros.macroTransformImpl
}

object SerializableTraitMacros {
  def macroTransformImpl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
    import c.universe._

    val circe = q"_root_.io.circe"
    val semiauto = q"$circe.generic.semiauto"

    def modifyObject(obj: Tree): Tree = obj match {
      case q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }" =>
        val className = tname.toTypeName
        val encoder = TermName(c.freshName(s"${tname}Encoder"))
        val decoder = TermName(c.freshName(s"${tname}Decoder"))

        q"""$mods object $tname extends { ..$earlydefns } with ..$parents { $self =>
          ..$body
          implicit val $encoder: $circe.Encoder[$className] = $semiauto.deriveEncoder[$className]
          implicit val $decoder: $circe.Decoder[$className] = $semiauto.deriveDecoder[$className]
        }"""
    }

    def modify(cls: Tree, obj: Tree): Tree = q"..${Seq(cls, modifyObject(obj))}"

    annottees match {
      case (cls: ClassDef) :: (obj: ModuleDef) :: Nil => modify(cls, obj)
      case (cls: ClassDef) :: Nil => modify(cls, q"object ${cls.name.toTermName}")
      case (obj: ModuleDef) :: Nil => modifyObject(obj) // this works for the companion object of a sealed trait or top-level case class but not nested case class
    }
  }
}
import io.circe.{Decoder, Encoder}

@serializableTrait
sealed trait SomeTrait

case class A(i: Int, s: String) extends SomeTrait

implicitly[Encoder[SomeTrait]] // compiles
implicitly[Decoder[SomeTrait]] // compiles

//scalac: object SomeTrait extends scala.AnyRef {
//  def <init>() = {
//    super.<init>();
//    ()
//  };
//  implicit val SomeTraitEncoder$macro$1: _root_.io.circe.Encoder[SomeTrait] = _root_.io.circe.generic.semiauto.deriveEncoder[SomeTrait];
//  implicit val SomeTraitDecoder$macro$2: _root_.io.circe.Decoder[SomeTrait] = _root_.io.circe.generic.semiauto.deriveDecoder[SomeTrait]
//}

If you want implicitly[Encoder[A]], implicitly[Decoder[A]] to compile too you should annotate case class A too.

The macro annotation is implemented now so that you can annotate either trait or its companion object (anyway the implicits will be generated in the companion object) but for some reason this doesn't work if you annotate the companion object of a case class (could not find Lazy implicit value of type io.circe.generic.encoding.DerivedAsObjectEncoder[A]). This seems to happen because I tested for nested SomeTrait and A, for top-level it's ok.

Actually, our macro annotation is similar to standard @JsonCodec

https://circe.github.io/circe/codecs/semiauto-derivation.html#jsoncodec

import io.circe.generic.JsonCodec

@JsonCodec 
sealed trait SomeTrait

@JsonCodec 
case class A(i: Int, s: String) extends SomeTrait

//  object SomeTrait extends scala.AnyRef {
//    def <init>() = {
//      super.<init>();
//      ()
//    };
//    implicit val codecForSomeTrait: AsObject[SomeTrait] = semiauto.deriveCodec[SomeTrait]
//  };
//  object A extends scala.AnyRef {
//    def <init>() = {
//      super.<init>();
//      ()
//    };
//    implicit val codecForA: AsObject[A] = semiauto.deriveCodec[A]
//  };

https://github.com/milessabin/shapeless/issues/1287

https://github.com/milessabin/shapeless/pull/1286

Schiro answered 9/11, 2022 at 4:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.