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 typeA
using the implicitBinaryCodec
forA
.Body.from[A]
— It encodes custom data of typeA
to a response body using the implicitBinaryCodec
forA
.
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:
Codecs | Schema Based BinaryCodec (zio.schema.codec package) | Output |
---|---|---|
JSON | JsonCodec.schemaBasedBinaryCodec[A](implicit schema: Schema[A]) | BinaryCodec[A] |
Protobuf | ProtobufCodec.protobufCodec[A](implicit schema: Schema[A]) | BinaryCodec[A] |
Avro | AvroCodec.schemaBasedBinaryCodec[A](implicit schema: Schema[A]) | BinaryCodec[A] |
Thrift | ThriftCodec.thriftBinaryCodec[A](implicit schema: Schema[A]) | BinaryCodec[A] |
MsgPack | MessagePackCodec.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:
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:
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