Skip to main content
Version: 2.x

PathCodec

PathCodec[A] represents a codec for paths of type A, comprising segments where each segment can be a literal, an integer, a long, a string, a UUID, or the trailing path.

The three basic operations that PathCodec supports are:

  • decode: converting a path into a value of type A.
  • format: converting a value of type A into a path.
  • ++ or /: combining two PathCodec values to create a new PathCodec that matches both paths, so the resulting of the decoding operation will be a tuple of the two values.

So we can think of PathCodec as the following simplified trait:

trait PathCodec[A] {
def /[B](that: PathCodec[B]): PathCodec[(A, B)]

def decode(path: Path): Either[String, A]
def format(value: A): : Either[String, Path]
}

Building PathCodecs

The PathCodec data type offers several predefined codecs for common types:

PathCodecDescription
PathCodec.boolA codec for a boolean path segment.
PathCodec.emptyA codec for an empty path.
PathCodec.literalA codec for a literal path segment.
PathCodec.longA codec for a long path segment.
PathCodec.stringA codec for a string path segment.
PathCodec.uuidA codec for a UUID path segment.

Complex PathCodecs can be constructed by combining them using the / operator:

import zio.http.codec.PathCodec
import PathCodec._

val pathCodec = empty / "users" / int("user-id") / "posts" / string("post-id")

By combining PathCodec values, the resulting PathCodec type reflects the types of the path segments it matches. In the provided example, the type of pathCodec is (Int, String) because it matches a path with two segments of type Int and String, respectively.

Decoding and Formatting PathCodecs

To decode a path into a value of type A, we can use the PathCodec#decode method:

import zio.http._

pathCodec.decode(Path("users/123/posts/abc"))
// res0: Either[String, (Int, String)] = Right(value = (123, "abc"))

To format (encode) a value of type A into a path, we can use the PathCodec#format method:

pathCodec.format((123, "abc"))
// res1: Either[String, Path] = Right(
// value = Path(flags = 1, segments = IndexedSeq("users", "123", "posts", "abc"))
// )

Rendering PathCodecs

If we render the previous PathCodec to a string using PathCodec#render or PathCodec#toString, we get the following result:

pathCodec.render
// res2: String = "/users/{user-id}/posts/{post-id}"

pathCodec.toString
// res3: String = "/users/{user-id}/posts/{post-id}"

Attaching Documentation to PathCodecs

The PathCodec#?? operator, takes a Doc and annotate the PathCodec with it. It is useful for generating developer-friendly documentation for the API:

import zio.http.codec._

val users = PathCodec.literal("users") ?? (Doc.p("Managing users including CRUD operations"))
// users: PathCodec[Unit] = Annotated(
// codec = Segment(segment = Literal(value = "users")),
// annotations = IndexedSeq(
// Documented(
// value = Paragraph(
// value = Text(value = "Managing users including CRUD operations")
// )
// )
// )
// )

When generating OpenAPI documentation, these annotations will be used to generate the API documentation.

Attaching Examples to PathCodecs

Similarly to attaching documentation, we can attach examples to PathCodec using the PathCodec#example operator:

import zio.http.codec._

val userId = PathCodec.int("user-id") ?? (Doc.p("The user id")) example ("user-id", 123)
// userId: PathCodec[Int] = Annotated(
// codec = Segment(segment = IntSeg(name = "user-id")),
// annotations = IndexedSeq(
// Documented(value = Paragraph(value = Text(value = "The user id"))),
// Examples(examples = Map("user-id" -> 123))
// )
// )

Using Value Objects with PathCodecs

Other than the common PathCodec constructors, it's also possible to transform a PathCodec into a more specific data type using the transform method.

This becomes particularly useful when adhering to domain-driven design principles and opting for value objects instead of primitive types:

import zio.http.codec.PathCodec
import PathCodec._

case class UserId private(value: Int)

object UserId {
def apply(value: Int): UserId =
if (value > 0)
new UserId(value)
else
throw new IllegalArgumentException("User id must be positive")
}


val userIdPathCodec: PathCodec[UserId] = int("user-id").transform(UserId.apply)(_.value)

This approach enables us to utilize the UserId value object in our routes, and the PathCodec will take care of the conversion between the path segment and the value object.

In the previous example, instead of throwing an exception, we can model the failure using the Either data type and then use the transformOrFailLeft to create a PathCodec:

import zio.http.codec.PathCodec
import PathCodec._

case class UserId private(value: Int)
object UserId {
def apply(value: Int): Either[String, UserId] =
if (value > 0)
Right(new UserId(value))
else
Left("User id must be positive")
}

val userIdPathCodec: PathCodec[UserId] = int("user-id").transformOrFailLeft(UserId.apply)(_.value)

Here is a list of the available transformation methods:

trait PathCodec[A] {
def transform[A2](f: A => A2)(g: A2 => A): PathCodec[A2]
def transformOrFail[A2](f: A => Either[String, A2])(g: A2 => Either[String, A]): PathCodec[A2]
def transformOrFailLeft[A2](f: A => Either[String, A2])(g: A2 => A): PathCodec[A2]
def transformOrFailRight[A2](f: A => A2)(g: A2 => Either[String, A]): PathCodec[A2]
}

Here is a complete example:

import zio._
import zio.http._
import zio.Cause.{Die, Stackless}
import zio.http.codec.PathCodec

object Main extends ZIOAppDefault {

import zio.http.codec.PathCodec
import PathCodec._

case class UserId private (value: Int)

object UserId {
def apply(value: Int): Either[String, UserId] =
if (value > 0)
Right(new UserId(value))
else
Left("User id must be greater than zero")
}

val userId: PathCodec[UserId] = int("user-id").transformOrFailLeft(UserId.apply)(_.value)

val routes: Routes[Any, Response] =
Routes(
Method.GET / "users" / userId ->
Handler.fromFunctionHandler[(UserId, Request)] { case (userId: UserId, request: Request) =>
Handler.text(userId.value.toString)
},
).handleErrorCause { case Stackless(cause, _) =>
cause match {
case Die(value, _) =>
if (value.getMessage == "User id must be greater than zero")
Response.badRequest(value.getMessage)
else
Response.internalServerError
}
}

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

Trailing Path Segments

Sometimes, there may be a need to match a path with a trailing segment, regardless of the number of segments it contains. This is where the trailing codec comes into play:

import zio._
import zio.http._

object TrailingExample extends ZIOAppDefault {
def staticFileHandler(path: Path): Handler[Any, Throwable, Request, Response] =
for {
file <- Handler.getResourceAsFile(path.encode)
http <-
if (file.isFile)
Handler.fromFile(file)
else
Handler.notFound
} yield http

val routes =
Routes(
Method.GET / "static" / trailing ->
Handler.fromFunctionHandler[(Path, Request)] { case (path: Path, _: Request) =>
staticFileHandler(path).contramap[(Path, Request)](_._2)
},
).sandbox @@ HandlerAspect.requestLogging()

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

In the provided example, if an incoming request matches the route pattern GET /static/*, the trailing codec will match the remaining path segments and bind them to the Path type. Therefore, a request to /static/foo/bar/baz.txt will match the route pattern, and the Path will be foo/bar/baz.txt.