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:
| Node | Meaning |
|---|---|
Segment | A single path segment, described by a SegmentCodec[A] |
Concat | Two path codecs composed sequentially with / or ++ |
Transform | Bidirectional type mapping over an existing codec |
Fallback | Two 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 = "").