Search code examples
jsonscalatraitscase-classcirce

Remove Case Class Definition in asJson output from Circe


Consider:

import io.circe.generic.auto._, io.circe.syntax._
sealed trait Data
case class Failed(foo: String, bar: String) extends Data
case class Success(foo1:String, bar1:String) extends Data
case class Task(Foo:String, Data: Data)

val something = Task("test", Failed("1", "2"))
println(something.asJson)

This outputs:

val something: Task = Task(test,Failed(1,2))
{"Foo" : "test", "Data" : {"Failed" : {"foo" : "1","bar" : "2"}}}

But what I really want, is it to output:

{"Foo" : "test", "Data" : {"foo" : "1", "bar" : "2"}}

Effectively, I just want to delete the "Failed" block but keep everything within that block.

Build info:

val scalaVer = "2.13.8"

lazy val circeJsonSchemaVersion = "0.2.0"
lazy val circeVersion = "0.14.3"
lazy val circeOpticsVersion = "0.14.1"

"io.circe" %% "circe-json-schema" % circeJsonSchemaVersion,
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-generic" % circeVersion,
"io.circe" %% "circe-parser" % circeVersion,
"io.circe" %% "circe-literal" % circeVersion,
"io.circe" %% "circe-generic-extras" % circeVersion,
"io.circe" %% "circe-optics" % circeOpticsVersion,

I have tried using @JsonCodec but wasn't able to get it working. I looked at custom codecs but that seems like it could be a giant rabbit hole.

EDIT: Fixed bad copy/paste in output


Solution

  • A more generic solution recommends

    import GenericDerivation.{ decodeEvent => _, encodeEvent => _ }
    
    object ShapesDerivation {
      import io.circe.shapes // "io.circe" %% "circe-shapes" % ...
      import shapeless.{ Coproduct, Generic }
    
      implicit def encodeAdtNoDiscr[A, Repr <: Coproduct](implicit
        gen: Generic.Aux[A, Repr],
        encodeRepr: Encoder[Repr]
      ): Encoder[A] = encodeRepr.contramap(gen.to)
    
      //...
    }
    

    but this seems to be outdated.

    You can try to derive encoders for non-labelled coproducts manually yourself (i.e. coproducts of the form Failed :+: Success :+: CNil rather than FieldType[Symbol @@ "Failed", Failed] :+: FieldType[Symbol @@ "Success", Success] :+: CNil)

    import io.circe.Encoder
    import shapeless.{:+:, CNil, Coproduct, Generic, Inl, Inr}
    
    trait NoLabelCoproductEncoders {
      implicit def noLabelEnc[A, Repr <: Coproduct](implicit
        gen: Generic.Aux[A, Repr],
        enc: NoLabelCoproductEncoder[Repr]
      ): Encoder[A] = enc.contramap(gen.to)
    }
    
    trait NoLabelCoproductEncoder[A <: Coproduct] extends Encoder[A]
    object NoLabelCoproductEncoder {
      implicit def ccons[H, T <: Coproduct](implicit
        hEnc: Encoder[H],
        tEnc: NoLabelCoproductEncoder[T]
      ): NoLabelCoproductEncoder[H :+: T] = {
        case Inl(h) => hEnc(h)
        case Inr(t) => tEnc(t)
      }
    
      implicit val cnil: NoLabelCoproductEncoder[CNil] = _.impossible
    }
    

    This works with semiauto

    import io.circe.generic.semiauto
    
    sealed trait Data
    object Data extends NoLabelCoproductEncoders
    
    case class Failed(foo: String, bar: String) extends Data
    object Failed {
      implicit val failedEnc: Encoder[Failed] = semiauto.deriveEncoder[Failed]
    }
    case class Success(foo1:String, bar1:String) extends Data
    object Success {
      implicit val successEnc: Encoder[Success] = semiauto.deriveEncoder[Success]
    }
    
    case class Task(Foo:String, Data: Data)
    object Task {
      implicit def taskEnc: Encoder[Task] = semiauto.deriveEncoder
    }
    
    import io.circe.syntax._
    
    val something = Task("test", Failed("1", "2"))
    something.asJson.noSpaces
    // {"Foo":"test","Data":{"foo":"1","bar":"2"}}
    

    and with auto

    sealed trait Data
    object Data extends NoLabelCoproductEncoders
    
    case class Failed(foo: String, bar: String) extends Data
    case class Success(foo1:String, bar1:String) extends Data
    
    case class Task(Foo:String, Data: Data)
    
    import io.circe.generic.auto._
    import Data._ // to make noLabelEnc of higher priority for Data, otherwise it's ambiguous with Circe auto._ encoders (in LowPriorityEncoders)
    import io.circe.syntax._
    
    val something = Task("test", Failed("1", "2"))
    something.asJson.noSpaces
    // {"Foo":"test","Data":{"foo":"1","bar":"2"}}