JSON handling
Add the JSON support module(s)
http4s-core does not include JSON support, but integration with three
popular Scala JSON libraries are supported as modules.
Circe
The http4s team recommends circe. Only http4s-circe is required for
basic interop with circe, but to follow this tutorial, install all three:
val http4sVersion = "0.21.32"
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-circe" % http4sVersion,
// Optional for auto-derivation of JSON codecs
"io.circe" %% "circe-generic" % "0.13.0",
// Optional for string interpolation to JSON model
"io.circe" %% "circe-literal" % "0.13.0"
)
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
Argonaut
Circe is a fork of argonaut, another popular JSON library in the Scala
community. The functionality is similar:
libraryDependencies += Seq(
"org.http4s" %% "http4s-argonaut" % http4sVersion,
// Optional for auto-derivation of JSON codecs
"com.github.alexarchambault" %% "argonaut-shapeless_6.2" % "1.2.0-M6"
)
Json4s
Json4s is less functionally pure than Circe or Argonaut, but older and
integrated with many Scala libraries. It comes with two backends.
You should pick one of these dependencies:
libraryDependencies += "org.http4s" %% "http4s-json4s-native" % http4sVersion
libraryDependencies += "org.http4s" %% "http4s-json4s-jackson" % http4sVersion
There is no extra codec derivation library for json4s, as it generally
bases its codecs on runtime reflection.
Sending raw JSON
Let’s create a function to produce a simple JSON greeting with circe. First, the imports:
import cats.effect._
import io.circe._
import io.circe.literal._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.implicits._
Then the actual code:
def hello(name: String): Json =
json"""{"hello": $name}"""
val greeting = hello("world")
// greeting: Json = JObject(object[hello -> "world"])
We now have a JSON value, but we don’t have enough to render it:
Ok(greeting).unsafeRunSync()
// error: Cannot convert from io.circe.Json to an Entity, because no EntityEncoder[cats.effect.IO, io.circe.Json] instance could be found.
// Ok(Hello("Alice").asJson).unsafeRunSync()
// ^
To encode a Scala value of type A
into an entity, we need an
EntityEncoder[A]
in scope. The http4s-circe module includes a
org.http4s.circe
object, which gives us exactly this for an
io.circe.Json
value:
import org.http4s.circe._
Ok(greeting).unsafeRunSync()
// res1: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Type: application/json, Content-Length: 17),
// Stream(..),
// io.chrisdavenport.vault.Vault@3ec23ae4
// )
The same EntityEncoder[Json]
we use on server responses is also
useful on client requests:
import org.http4s.client.dsl.io._
POST(json"""{"name": "Alice"}""", uri"/hello").unsafeRunSync()
// res2: Request[IO] = (
// Method("POST"),
// Uri(None, None, "/hello", , None),
// HttpVersion(1, 1),
// Headers(Content-Type: application/json, Content-Length: 16),
// Stream(..),
// io.chrisdavenport.vault.Vault@6d636c30
// )
Encoding case classes as JSON
These JSON literals are nice, but in real apps, we prefer to operate
on case classes and use JSON as a serialization format near the edge
of the world.
Let’s define a couple case classes:
case class Hello(name: String)
case class User(name: String)
To transform a value of type A
into Json
, circe uses an
io.circe.Encoder[A]
. With circe’s syntax, we can convert any value
to JSON as long as an implicit Encoder
is in scope:
import io.circe.syntax._
Hello("Alice").asJson
// error: could not find implicit value for parameter encoder: io.circe.Encoder[repl.MdocSession.App.Hello]
// Hello("Alice").asJson
// ^^^^^^^^^^^^^^^^^^^^^
Oops! We haven’t told Circe how we want to encode our case class.
Let’s provide an encoder:
implicit val HelloEncoder: Encoder[Hello] =
Encoder.instance { (hello: Hello) =>
json"""{"hello": ${hello.name}}"""
}
// HelloEncoder: Encoder[Hello] = io.circe.Encoder$$anon$3@2a15f153
Hello("Alice").asJson
// res4: Json = JObject(object[hello -> "Alice"])
That was easy, but gets tedious for applications dealing in lots of
types. Fortunately, circe can automatically derive an encoder for us,
using the field names of the case class as key names in a JSON object:
import io.circe.generic.auto._
User("Alice").asJson
// res5: Json = JObject(object[name -> "Alice"])
Equipped with an Encoder
and .asJson
, we can send JSON in requests
and responses for our case classes:
Ok(Hello("Alice").asJson).unsafeRunSync()
// res6: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Type: application/json, Content-Length: 17),
// Stream(..),
// io.chrisdavenport.vault.Vault@5afb5f6a
// )
POST(User("Bob").asJson, uri"/hello").unsafeRunSync()
// res7: Request[IO] = (
// Method("POST"),
// Uri(None, None, "/hello", , None),
// HttpVersion(1, 1),
// Headers(Content-Type: application/json, Content-Length: 14),
// Stream(..),
// io.chrisdavenport.vault.Vault@6633b553
// )
If within some route we serve json only, we can use:
{
import org.http4s.circe.CirceEntityEncoder._
}
Thus there’s no more need in calling asJson
on result.
However, it may introduce ambiguity errors when we also build
some json by hand within the same scope.
Receiving raw JSON
Just as we needed an EntityEncoder[JSON]
to send JSON from a server
or client, we need an EntityDecoder[JSON]
to receive it.
The org.http4s.circe._
package provides an implicit
EntityDecoder[Json]
. This makes it very easy to decode a request or
response body to JSON using the as
syntax:
Ok("""{"name":"Alice"}""").flatMap(_.as[Json]).unsafeRunSync()
// res9: Json = JObject(object[name -> "Alice"])
POST("""{"name":"Bob"}""", uri"/hello").flatMap(_.as[Json]).unsafeRunSync()
// res10: Json = JObject(object[name -> "Bob"])
Like sending raw JSON, this is useful to a point, but we typically
want to get to a typed model as quickly as we can.
Decoding JSON to a case class
To get from an HTTP entity to Json
, we use an EntityDecoder[Json]
.
To get from Json
to any type A
, we need an io.circe.Decoder[A]
.
http4s-circe provides the jsonOf
function to make the connection all
the way from HTTP to your type A
. Specifically, jsonOf[A]
takes
an implicit Decoder[A]
and makes a EntityDecoder[A]
:
implicit val userDecoder = jsonOf[IO, User]
// userDecoder: EntityDecoder[IO, User] = org.http4s.EntityDecoder$$anon$2@5381e928
Ok("""{"name":"Alice"}""").flatMap(_.as[User]).unsafeRunSync()
// res11: User = User("Alice")
POST("""{"name":"Bob"}""", uri"/hello").flatMap(_.as[User]).unsafeRunSync()
// res12: User = User("Bob")
If we are always decoding from JSON to a typed model, we can use
the following import:
import org.http4s.circe.CirceEntityDecoder._
This creates an EntityDecoder[A]
for every A
that has a Decoder
instance.
However, be cautious when using this. Having this implicit
in scope does mean that we would always try to decode HTTP entities
from JSON (even if it is XML or plain text, for instance).
For more convenience there is import combining both encoding
and decoding derivation:
import org.http4s.circe.CirceEntityCodec._
Putting it all together
A Hello world service
Our hello world service will parse a User
from a request and offer a
proper greeting.
import cats.effect._
import io.circe.generic.auto._
import io.circe.syntax._
import org.http4s._
import org.http4s.circe._
import org.http4s.dsl.io._
import org.http4s.server.blaze._
import org.http4s.implicits._
import scala.concurrent.ExecutionContext.Implicits.global
case class User(name: String)
case class Hello(greeting: String)
// Needed by `BlazeServerBuilder`. Provided by `IOApp`.
implicit val cs: ContextShift[IO] = IO.contextShift(global)
implicit val timer: Timer[IO] = IO.timer(global)
implicit val decoder = jsonOf[IO, User]
val jsonApp = HttpRoutes.of[IO] {
case req @ POST -> Root / "hello" =>
for {
// Decode a User request
user <- req.as[User]
// Encode a hello response
resp <- Ok(Hello(user.name).asJson)
} yield (resp)
}.orNotFound
val server = BlazeServerBuilder[IO](global).bindHttp(8080).withHttpApp(jsonApp).resource
val fiber = server.use(_ => IO.never).start.unsafeRunSync()
A Hello world client
Now let’s make a client for the service above:
import org.http4s.client.dsl.io._
import org.http4s.client.blaze._
import cats.effect.IO
import io.circe.generic.auto._
import fs2.Stream
// Decode the Hello response
def helloClient(name: String): Stream[IO, Hello] = {
// Encode a User request
val req = POST(User(name).asJson, uri"http://localhost:8080/hello")
// Create a client
BlazeClientBuilder[IO](global).stream.flatMap { httpClient =>
// Decode a Hello response
Stream.eval(httpClient.expect(req)(jsonOf[IO, Hello]))
}
}
Finally, we post User("Alice")
to our Hello service and expect
Hello("Alice")
back:
val helloAlice = helloClient("Alice")
// helloAlice: Stream[IO, Hello] = Stream(..)
helloAlice.compile.last.unsafeRunSync()
// res14: Option[Hello] = Some(Hello("Alice"))
Finally, shut down our example server.
fiber.cancel.unsafeRunSync()