Skip to main content
Version: 2.x

BinaryCodecs for Request/Response Bodies

ZIO HTTP has built-in support for encoding and decoding request/response bodies. This is achieved using generating codecs for our custom data types powered by ZIO Schema.

ZIO Schema is a library for defining the schema for any custom data type, including case classes, sealed traits, and enumerations, other than the built-in types. It provides a way to derive codecs for these custom data types, for encoding and decoding data to/from JSON, Protobuf, Avro, and other formats.

Having codecs for our custom data types allows us to easily serialize/deserialize data to/from request/response bodies in our HTTP applications.

The Body data type in ZIO HTTP represents the body message of a request or a response. It has two main functionality for encoding and decoding request/response bodies, both of which require an implicit BinaryCodec for the corresponding data type:

  • Body#to[A] — It decodes the request body to a custom data of type A using the implicit BinaryCodec for A.
  • Body.from[A] — It encodes custom data of type A to a response body using the implicit BinaryCodec for A.
trait Body {
def to[A](implicit codec: BinaryCodec[A]): Task[A] = ???
}

object Body {
def from[A](a: A)(implicit codec: BinaryCodec[A]): Body = ???
}

To use these two methods, we need to have an implicit BinaryCodec for our custom data type, A. Let's assume we have a Book case class with title, authors fields:

case class Book(title: String, authors: List[String])

To create a BinaryCodec[Book] for our Book case class, we can implement the BinaryCodec interface:

import zio._ 
import zio.stream._
import zio.schema.codec._

implicit val bookBinaryCodec = new BinaryCodec[Book] {
override def encode(value: Book): Chunk[Byte] = ???
override def streamEncoder: ZPipeline[Any, Nothing, Book, Byte] = ???
override def decode(whole: Chunk[Byte]): Either[DecodeError, Book] = ???
override def streamDecoder: ZPipeline[Any, DecodeError, Byte, Book] = ???
}

Now, when we call Body.from(Book("Zionomicon", List("John De Goes"))), it will encode the Book case class to a response body using the implicit BinaryCodec[Book]. But, what happens if we add a new field to the Book case class, or change one of the existing fields? We would need to update the BinaryCodec[Book] implementation to reflect these changes. Also, if we want to support body response bodies with multiple book objects, we would need to implement a new codec for List[Book]. So, maintaining these codecs can be cumbersome and error-prone.

ZIO Schema simplifies this process by providing a way to derive codecs for our custom data types. For each custom data type, A, if we write/derive a Schema[A] using ZIO Schema, then we can derive a BinaryCodec[A] for any format supported by ZIO Schema, including JSON, Protobuf, Avro, and Thrift.

So, let's generate a Schema[Book] for our Book case class:

import zio.schema._

object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen[Book]
}

Based on what format we want, we can add one of the following codecs to our build.sbt file:

libraryDependencies += "dev.zio" %% "zio-schema-json"     % "1.4.1"
libraryDependencies += "dev.zio" %% "zio-schema-protobuf" % "1.4.1"
libraryDependencies += "dev.zio" %% "zio-schema-avro" % "1.4.1"
libraryDependencies += "dev.zio" %% "zio-schema-thrift" % "1.4.1"

After adding the required codec's dependency, we can import the right binary codec inside the zio.schema.codec package:

CodecsSchema Based BinaryCodec (zio.schema.codec package)Output
JSONJsonCodec.schemaBasedBinaryCodec[A](implicit schema: Schema[A])BinaryCodec[A]
ProtobufProtobufCodec.protobufCodec[A](implicit schema: Schema[A])BinaryCodec[A]
AvroAvroCodec.schemaBasedBinaryCodec[A](implicit schema: Schema[A])BinaryCodec[A]
ThriftThriftCodec.thriftBinaryCodec[A](implicit schema: Schema[A])BinaryCodec[A]
MsgPackMessagePackCodec.messagePackCodec[A](implicit schema: Schema[A])BinaryCodec[A]

That is very simple! To have a BinaryCodec of type A we only need to derive a Schema[A] and then use an appropriate codec from the zio.schema.codec package.

JSON Codec Example

JSON Serialization of Response Body

Assume want to write an HTTP API that returns a list of books in JSON format:

zio-http-example/src/main/scala/example/codecs/ResponseBodyJsonSerializationExample.scala
package example.codecs

import zio._

import zio.schema.codec.JsonCodec.schemaBasedBinaryCodec
import zio.schema.{DeriveSchema, Schema}

import zio.http._

object ResponseBodyJsonSerializationExample extends ZIOAppDefault {

case class Book(title: String, authors: List[String])

object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen
}

val book1 = Book("Programming in Scala", List("Martin Odersky", "Lex Spoon", "Bill Venners", "Frank Sommers"))
val book2 = Book("Zionomicon", List("John A. De Goes", "Adam Fraser"))
val book3 = Book("Effect-Oriented Programming", List("Bill Frasure", "Bruce Eckel", "James Ward"))

val routes: Routes[Any, Nothing] =
Routes(
Method.GET / "users" ->
handler(Response(body = Body.from(List(book1, book2, book3)))),
)

def run = Server.serve(routes).provide(Server.default)
}

JSON Deserialization of Request Body

In the example below, we have an HTTP API that accepts a JSON request body containing a Book object and adds it to a list of books:

zio-http-example/src/main/scala/example/codecs/RequestBodyJsonDeserializationExample.scala
package example.codecs

import zio._

import zio.schema.codec.JsonCodec.schemaBasedBinaryCodec
import zio.schema.{DeriveSchema, Schema}

import zio.http._

object RequestBodyJsonDeserializationExample extends ZIOAppDefault {

case class Book(title: String, authors: List[String])

object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen
}

val routes: Routes[Ref[List[Book]], Nothing] =
Routes(
Method.POST / "books" ->
handler { (req: Request) =>
for {
book <- req.body.to[Book].catchAll(_ => ZIO.fail(Response.badRequest("unable to deserialize the request")))
books <- ZIO.service[Ref[List[Book]]]
_ <- books.updateAndGet(_ :+ book)
} yield Response.ok
},
Method.GET / "books" ->
handler { (_: Request) =>
ZIO
.serviceWithZIO[Ref[List[Book]]](_.get)
.map(books => Response(body = Body.from(books)))
},
)

def run = Server.serve(routes).provide(Server.default, ZLayer.fromZIO(Ref.make(List.empty[Book])))
}

To send a POST request to the /books endpoint with a JSON body containing a Book object, we can use the following curl command:

$ curl -X POST -d '{"title": "Zionomicon", "authors": ["John De Goes", "Adam Fraser"]}' http://localhost:8080/books

After sending the POST request, we can retrieve the list of books by sending a GET request to the /books endpoint:

$ curl http://localhost:8080/books