Skip to main content

PathCodec

PathCodec[A] is a composable descriptor for URL path structures. It holds a tree of segment codecs connected by concatenation and fallback nodes, and provides bidirectional path conversion: PathCodec#decode extracts a typed value from a Path, returning Either[String, A] (the typed value or error), and PathCodec#format formats a typed value back to a Path, returning Either[String, Path] (the path or error). Its definition is:

sealed trait PathCodec[A]

Motivation

URL paths need both matching and generation. A routing library that only matches paths requires a separate URL-builder for links and redirects, leading to duplication and drift. PathCodec is bidirectional: every codec that can decode /users/42 into 42: Int can also format 42 back into /users/42. This makes it safe to use the same path definition for routing, link generation, and OpenAPI path parameter documentation.

Structure

PathCodec is an ADT with four node types:

NodeMeaning
SegmentA single path segment, described by a SegmentCodec[A]
ConcatTwo path codecs composed sequentially with / or ++
TransformBidirectional type mapping over an existing codec
FallbackTwo literal alternatives (applies only with orElse)

Construction

We build a PathCodec from smart constructors that produce typed segment nodes, from a path string, or by wrapping a SegmentCodec directly.

Predefined segment constructors

The most common path building blocks are available as smart constructors on the companion:

import zio.blocks.endpoint._
import zio.blocks.endpoint.RoutePattern._

val literalUsers: PathCodec[Unit] = PathCodec.literal("users")
val intId: PathCodec[Int] = PathCodec.int("id")
val longId: PathCodec[Long] = PathCodec.long("id")
val stringSlug: PathCodec[String] = PathCodec.string("slug")
val uuidId: PathCodec[java.util.UUID] = PathCodec.uuid("id")
val boolFlag: PathCodec[Boolean] = PathCodec.bool("enabled")
val rest: PathCodec[zio.http.Path] = PathCodec.trailing

PathCodec.literal is a macro that validates the value at compile time — it rejects empty strings and strings containing / or characters requiring URL encoding.

From a string

To build a codec from a slash-separated string of literal segments, use PathCodec.apply:

import zio.blocks.endpoint._
import zio.blocks.endpoint.RoutePattern._

val path: PathCodec[Unit] = PathCodec("/api/v1/users")

This is equivalent to concatenating PathCodec.literal for each segment.

From a SegmentCodec

To wrap a custom SegmentCodec[A] into a PathCodec[A], use PathCodec.apply(segment):

import zio.blocks.endpoint._
import zio.blocks.endpoint.RoutePattern._

val combined = PathCodec(SegmentCodec.literal("v") ~ SegmentCodec.int("version"))

There is also an implicit conversion from SegmentCodec[A] to PathCodec[A] and from String to PathCodec[Unit], so both can appear directly in / expressions.

Composition

Path codecs compose in two ways: sequential concatenation with / or ++, and literal alternatives with orElse.

Sequential composition with / and ++

/ and ++ are equivalent: both concatenate two path codecs. The result type is flattened automatically (so Unit / Int gives Int, not (Unit, Int)), eliminating Unit components:

import zio.blocks.endpoint._
import zio.blocks.endpoint.RoutePattern._

val route: PathCodec[Int] = PathCodec.literal("users") / PathCodec.int("id")

In the context of a RoutePattern, the same operator works directly:

import zio.blocks.endpoint._
import zio.blocks.endpoint.RoutePattern._
import zio.http.Method

val pattern = Method.GET / "users" / PathCodec.int("id") / "posts"

Literal alternatives with orElse

To match either of two literal segments, use orElse:

import zio.blocks.endpoint._
import zio.blocks.endpoint.RoutePattern._

val either: PathCodec[Unit] =
PathCodec.literal("users").orElse(PathCodec.literal("members"))

orElse accepts any PathCodec[Unit] at the type level, but PathCodec#alternatives throws an IllegalStateException at runtime if a non-literal segment appears in an alternative branch. In practice, only literal segments are safe inside orElse.

Decoding and Formatting

PathCodec is bidirectional: PathCodec#decode turns a runtime Path into a typed value, and PathCodec#format turns a typed value back into a Path.

PathCodec#decode

To extract a typed value from a runtime Path:

import zio.blocks.endpoint._
import zio.blocks.endpoint.RoutePattern._
import zio.http.Path

val codec = PathCodec.int("id")

val result: Either[String, Int] = codec.decode(Path("/42"))

PathCodec#decode returns Left(message) when no segment matches or a segment cannot be parsed.

PathCodec#format

To turn a typed value into a Path:

import zio.blocks.endpoint._
import zio.blocks.endpoint.RoutePattern._
import zio.http.Path

val codec = PathCodec.uuid("id")

val path: Either[String, Path] =
codec.format(java.util.UUID.fromString("550e8400-e29b-41d4-a716-446655440000"))

PathCodec#matches

To test whether a Path matches without extracting a value:

import zio.blocks.endpoint._
import zio.blocks.endpoint.RoutePattern._
import zio.http.Path

val codec = PathCodec.literal("users")
val matched = codec.matches(Path("/users"))

Type Transformations

Use these methods to map the typed value that PathCodec decodes or encodes without changing the underlying path structure.

PathCodec#transform

To map the decoded value to a different type without changing the path structure, use PathCodec#transform. Both directions must be total:

import zio.blocks.endpoint._
import zio.blocks.endpoint.RoutePattern._
import zio.blocks.endpoint.PathCodec._

final case class UserId(value: Int)

val userIdCodec: PathCodec[UserId] =
PathCodec.int("id").transform[UserId](UserId(_), _.value)

PathCodec#transformOrFail

When decoding or encoding can fail, use PathCodec#transformOrFail. A Left from the decode function causes the path not to match:

import zio.blocks.endpoint._
import zio.blocks.endpoint.RoutePattern._

val nonNegativeInt: PathCodec[Int] =
PathCodec.int("count").transformOrFail[Int](
n => if (n >= 0) Right(n) else Left(s"Expected non-negative, got $n"),
n => Right(n)
)

Rendering

PathCodec#render produces a human-readable path string. Dynamic segments appear as {name} by default:

import zio.blocks.endpoint._
import zio.blocks.endpoint.RoutePattern._

val codec = PathCodec.literal("users") / PathCodec.int("id")
val rendered: String = codec.render

To use a different prefix/suffix for dynamic segments (e.g., :id for Express-style paths), call the lower-level PathCodec.render(codec, prefix = ":", suffix = "").