Search code examples
jsonscalashapelesshlistcirce

Parse a case class containing an HList into a JSON string, using Circe


I'm doing a thing in Scala. I have the following case class:

import shapeless._
case class Foo(param1: String, param2: HList)

I'd like to obtain a JSON representation of this type, using Circe. I'd also like to map the resulting JSON string back to the type.

The module circe-shapes does automatic derivation of HLists, and it's easy to to from HList to JSON and back. See this example:

scala> import shapeless._
import shapeless._

scala> import io.circe._, io.circe.generic.auto._, io.circe.parser._, io.circe.syntax._
import io.circe._
import io.circe.generic.auto._
import io.circe.parser._
import io.circe.syntax._

scala> import io.circe.shapes._
import io.circe.shapes._

scala> val myList = 30 :: "car" :: HNil
myList: shapeless.::[Int,shapeless.::[String,shapeless.HNil]] = 30 :: car :: HNil

scala> val listJson = myList.asJson
listJson: io.circe.Json =
[
  30,
  "car"
]

scala> listJson.as[HList] // won't work
<console>:32: error: could not find implicit value for parameter d: io.circe.Decoder[shapeless.HList]
       listJson.as[HList]
                  ^

scala> listJson.as[::[Int, ::[String, HNil]]]
res3: io.circe.Decoder.Result[shapeless.::[Int,shapeless.::[String,shapeless.HNil]]] = Right(30 :: car :: HNil)

Case classes containing "standard" types are also trivial:

scala> case class Bar(one: String, a: Double, andAn: Int)
defined class Bar

scala> val myBar = Bar("pie", 4.35, 2)
myBar: Bar = Bar(pie,4.35,2)

scala> val barJson = myBar.asJson
barJson: io.circe.Json =
{
  "one" : "pie",
  "a" : 4.35,
  "andAn" : 2
}

scala> barJson.as[Bar]
res5: io.circe.Decoder.Result[Bar] = Right(Bar(pie,4.35,2))

Being explicit with the type of the HList works wonders, but it kinda defeats the purpose of the HList:

scala> case class Foo2(a: String, b: ::[Int, ::[String, HNil]])
defined class Foo2

scala> val myFoo2 = Foo2("ark", 42 :: "meg" :: HNil)
myFoo2: Foo2 = Foo2(ark,42 :: meg :: HNil)

scala> val foo2Json = myFoo2.asJson
foo2Json: io.circe.Json =
{
  "a" : "ark",
  "b" : [
    42,
    "meg"
  ]
}

scala> foo2Json.as[Foo2]
res8: io.circe.Decoder.Result[Foo2] = Right(Foo2(ark,42 :: meg :: HNil))

Can Circe decode an arbitrary HList?


Solution

  • Yes, circe can do this, but you'll need to change your case class to make it hold on to more information about the HList:

    import shapeless._
    
    case class Foo[L <: HList](param1: String, param2: L)
    

    And then the imports:

    import io.circe.generic.auto._, io.circe.shapes._, io.circe.parser._, io.circe.syntax._
    

    And then you can call asJson, etc.:

    scala> val foo = Foo("ark", 42 :: "meg" :: HNil)
    foo: Foo[shapeless.::[Int,shapeless.::[String,shapeless.HNil]]] = Foo(ark,42 :: meg :: HNil)
    
    scala> foo.asJson
    res0: io.circe.Json =
    {
      "param1" : "ark",
      "param2" : [
        42,
        "meg"
      ]
    }
    
    scala> decode[Foo[Int :: String :: HNil]](res0.noSpaces).right.foreach(println)
    Foo(ark,42 :: meg :: HNil)
    

    In this case specifically, the derivation mechanisms in circe need the static information about the elements of the hlist in order to produce encoders and decoders, but more generally when you're working with hlists you'll want to avoid having members or values typed as plain old HList.

    There just isn't very much you can do with something that's of type HList. You can prepend values to it, and you can get a string representation via toString, but that's about it—you can't use any of Shapeless's ops type classes, you can't recover any information about the individual types, etc. You'll almost always (probably always always) want an L <: HList, which allows you to keep the information that makes the type useful.