jsonscalacircehttp4shttp4s-circe

Encoding recursive data structure into Json with Circe when running on http4s


I am building a very simply service, that should return a tree like structure defined through a recursive case class:

case class Node(id: Int, name: String, children: Seq[Node] = Seq())

But for some reason I keep getting a following compilation error:

Error:(24, 70) could not find implicit value for parameter encoder: io.circe.Encoder[Seq[com.ansarada.ds.docviewer.server.Main.Node]]
implicit val nodesEncoder: EntityEncoder[Seq[Node]] = jsonEncoderOf[Seq[Node]]

Error:(24, 70) not enough arguments for method jsonEncoderOf: (implicit encoder: io.circe.Encoder[Seq[com.ansarada.ds.docviewer.server.Main.Node]])org.http4s.EntityEncoder[Seq[com.ansarada.ds.docviewer.server.Main.Node]]. Unspecified value parameter encoder. implicit val nodesEncoder: EntityEncoder[Seq[Node]] = jsonEncoderOf[Seq[Node]]

The code is compiled, once I remove a children element definition and turn a Node into a flat object:

case class Node(id: Int, name: String)

Can anyone help me to define the correct Json encoder for a case with nested children?

Full code:

import org.http4s.circe._
import org.http4s.dsl._
import org.http4s.server.blaze.BlazeBuilder
import org.http4s.server.{Server, ServerApp}
import org.http4s.{EntityEncoder, HttpService}

import scalaz.concurrent.Task

object Main extends ServerApp {
  import io.circe.generic.auto._
  import io.circe.syntax._

  case class Node(id: Int, name: String, children: Seq[Node] = Seq())

  def getNodes: Seq[Node] = Seq(
    Node(0, "#One"),
    Node(1, "#Two"),
    Node(2, "#Three")
  )

  implicit val nodeEncoder: EntityEncoder[Node] = jsonEncoderOf[Node]
  implicit val nodesEncoder: EntityEncoder[Seq[Node]] = jsonEncoderOf[Seq[Node]]

  override def server(args: List[String]): Task[Server] = {
    val nodesService = HttpService {
      case _ @ GET -> Root / "nodes" =>
        Ok(getNodes.asJson)
    }

    BlazeBuilder
      .bindHttp(8080, "localhost")
      .mountService(nodesService, "/api")
      .start
  }
}

Solution

  • Ok. Got it working using circe json annotations - @JsonCodec. Step-by-step guide to fix an issue described above:

    1. Prerequisites - import circe lib:

      "io.circe" %% "circe-core" % "0.8.0",
      "io.circe" %% "circe-generic" % "0.8.0",
      "io.circe" %% "circe-literal" % "0.8.0",
      "io.circe" %% "circe-parser" % "0.8.0",
      
    2. Turn auto compiler plugins in your build.sbt:

      autoCompilerPlugins := true
      
    3. Add scalamacros paradise compiler plugin:

      lazy val root = Project("root", file("."))
          .settings(
              addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.patch)
      )
      
    4. Annotate case classes with @JsonCodec:

      @JsonCodec case class Node(id: Int, children: Seq[Node] = Seq())
      
    5. Cast response to json:

      Ok(getNodes.asJson)
      

    Full code snippet:

    import org.http4s.circe._
    import org.http4s.dsl._
    import org.http4s.server.blaze.BlazeBuilder
    import org.http4s.server.{Server, ServerApp}
    import org.http4s.{EntityEncoder, HttpService}
    
    import scalaz.concurrent.Task
    object Main extends ServerApp {
      import io.circe.generic.auto._
      import io.circe.syntax._
    
      @JsonCodec case class Node(id: Int, name: String, children: Seq[Node] = Seq())
    
      def getNodes: Seq[Node] = Seq(
        Node(0, "#One"),
        Node(1, "#Two"),
        Node(2, "#Three")
      )
    
      override def server(args: List[String]): Task[Server] = {
        val nodesService = HttpService {
          case _ @ GET -> Root / "nodes" =>
            Ok(getNodes.asJson)
        }
    
        BlazeBuilder
          .bindHttp(8080, "localhost")
          .mountService(nodesService, "/api")
          .start
      }
    }