Type-safe query parameter extraction and validation with support for optional parameters, multi-value parameters, custom decoders, and validation with detailed error reporting.
Fundamental query parameter extractor that provides access to all query parameters.
/**
* Query parameter extractor
* Extracts request and its query parameters as a Map
*/
object :? {
def unapply[F[_]](req: Request[F]): Some[(Request[F], Map[String, collection.Seq[String]])]
}Usage Examples:
val routes = HttpRoutes.of[IO] {
// Access all query parameters
case GET -> Root / "search" :? params =>
val query = params.get("q").flatMap(_.headOption).getOrElse("")
Ok(s"Search query: $query")
// Pattern match with specific extractors
case GET -> Root / "users" :? Limit(limit) +& Offset(offset) =>
Ok(s"Limit: $limit, Offset: $offset")
}Combinator for extracting multiple query parameters from the same request.
/**
* Multiple parameter extractor combinator
* Allows chaining multiple parameter extractors
*/
object +& {
def unapply(params: Map[String, collection.Seq[String]]):
Some[(Map[String, collection.Seq[String]], Map[String, collection.Seq[String]])]
}Usage Examples:
object Limit extends QueryParamDecoderMatcher[Int]("limit")
object Offset extends QueryParamDecoderMatcher[Int]("offset")
object SortBy extends QueryParamDecoderMatcher[String]("sort")
val routes = HttpRoutes.of[IO] {
// Combine multiple parameters
case GET -> Root / "users" :? Limit(limit) +& Offset(offset) =>
Ok(s"Pagination: limit=$limit, offset=$offset")
// Chain many parameters
case GET -> Root / "search" :? Query(q) +& Limit(limit) +& SortBy(sort) =>
Ok(s"Search: $q, limit: $limit, sort: $sort")
}Type-safe query parameter extraction with automatic type conversion.
/**
* Basic query parameter matcher with type conversion
* Uses QueryParamDecoder for type-safe conversion
*/
abstract class QueryParamDecoderMatcher[T: QueryParamDecoder](name: String) {
def unapply(params: Map[String, collection.Seq[String]]): Option[T]
def unapplySeq(params: Map[String, collection.Seq[String]]): Option[collection.Seq[T]]
}Usage Examples:
// Define parameter matchers
object UserId extends QueryParamDecoderMatcher[Int]("user_id")
object Active extends QueryParamDecoderMatcher[Boolean]("active")
object Tags extends QueryParamDecoderMatcher[String]("tags")
val routes = HttpRoutes.of[IO] {
// Single value extraction
case GET -> Root / "user" :? UserId(id) =>
Ok(s"User ID: $id")
// Boolean parameters
case GET -> Root / "users" :? Active(isActive) =>
Ok(s"Active users only: $isActive")
// Multiple values of same parameter
case GET -> Root / "posts" :? Tags.unapplySeq(tags) =>
Ok(s"Tags: ${tags.mkString(", ")}")
}Simplified matcher that uses implicit QueryParam for parameter name resolution.
/**
* Query parameter matcher using implicit QueryParam for name resolution
*/
abstract class QueryParamMatcher[T: QueryParamDecoder: QueryParam]
extends QueryParamDecoderMatcher[T](QueryParam[T].key.value)Usage Examples:
// Define parameter with implicit QueryParam
case class Limit(value: Int)
implicit val limitParam: QueryParam[Limit] = QueryParam.fromKey("limit")
implicit val limitDecoder: QueryParamDecoder[Limit] =
QueryParamDecoder[Int].map(Limit.apply)
object LimitMatcher extends QueryParamMatcher[Limit]
val routes = HttpRoutes.of[IO] {
case GET -> Root / "data" :? LimitMatcher(limit) =>
Ok(s"Limit: ${limit.value}")
}Handle optional query parameters that may or may not be present.
/**
* Optional query parameter matcher
* Returns Some(Some(value)) if parameter present and valid
* Returns Some(None) if parameter absent
* Returns None if parameter present but invalid
*/
abstract class OptionalQueryParamDecoderMatcher[T: QueryParamDecoder](name: String) {
def unapply(params: Map[String, collection.Seq[String]]): Option[Option[T]]
}Usage Examples:
object OptionalLimit extends OptionalQueryParamDecoderMatcher[Int]("limit")
object OptionalSort extends OptionalQueryParamDecoderMatcher[String]("sort")
val routes = HttpRoutes.of[IO] {
case GET -> Root / "users" :? OptionalLimit(limitOpt) +& OptionalSort(sortOpt) =>
val limit = limitOpt.getOrElse(10)
val sort = sortOpt.getOrElse("name")
Ok(s"Limit: $limit, Sort: $sort")
}Provide default values for missing query parameters.
/**
* Query parameter matcher with default value
* Returns default value if parameter is missing
* Returns None if parameter is present but invalid
*/
abstract class QueryParamDecoderMatcherWithDefault[T: QueryParamDecoder](name: String, default: T) {
def unapply(params: Map[String, collection.Seq[String]]): Option[T]
}
abstract class QueryParamMatcherWithDefault[T: QueryParamDecoder: QueryParam](default: T)
extends QueryParamDecoderMatcherWithDefault[T](QueryParam[T].key.value, default)Usage Examples:
object LimitWithDefault extends QueryParamDecoderMatcherWithDefault[Int]("limit", 10)
object PageWithDefault extends QueryParamDecoderMatcherWithDefault[Int]("page", 1)
val routes = HttpRoutes.of[IO] {
case GET -> Root / "data" :? LimitWithDefault(limit) +& PageWithDefault(page) =>
Ok(s"Page: $page, Limit: $limit")
// ?limit=20&page=3 -> "Page: 3, Limit: 20"
// ?page=2 -> "Page: 2, Limit: 10" (default limit)
// (no params) -> "Page: 1, Limit: 10" (both defaults)
}Handle query parameters that can appear multiple times.
/**
* Multi-value query parameter matcher
* Handles parameters that appear multiple times in the query string
* Returns Validated result with all values or parse errors
*/
abstract class OptionalMultiQueryParamDecoderMatcher[T: QueryParamDecoder](name: String) {
def unapply(params: Map[String, collection.Seq[String]]):
Option[ValidatedNel[ParseFailure, List[T]]]
}Usage Examples:
import cats.data.Validated
import cats.data.Validated.{Invalid, Valid}
object Tags extends OptionalMultiQueryParamDecoderMatcher[String]("tag")
object Ids extends OptionalMultiQueryParamDecoderMatcher[Int]("id")
val routes = HttpRoutes.of[IO] {
// Handle multiple tag parameters: ?tag=scala&tag=http4s&tag=web
case GET -> Root / "posts" :? Tags(tagResult) =>
tagResult match {
case Valid(tags) => Ok(s"Tags: ${tags.mkString(", ")}")
case Invalid(errors) => BadRequest(s"Invalid tags: ${errors.toList.mkString(", ")}")
}
// Multiple ID parameters: ?id=1&id=2&id=3
case GET -> Root / "users" :? Ids(idResult) =>
idResult match {
case Valid(ids) => Ok(s"User IDs: ${ids.mkString(", ")}")
case Invalid(_) => BadRequest("Invalid user IDs")
}
}Get detailed validation results with error information.
/**
* Validating query parameter matcher
* Returns validation result with detailed error information
*/
abstract class ValidatingQueryParamDecoderMatcher[T: QueryParamDecoder](name: String) {
def unapply(params: Map[String, collection.Seq[String]]):
Option[ValidatedNel[ParseFailure, T]]
}Usage Examples:
import cats.data.Validated.{Invalid, Valid}
object ValidatingAge extends ValidatingQueryParamDecoderMatcher[Int]("age")
object ValidatingEmail extends ValidatingQueryParamDecoderMatcher[String]("email")
val routes = HttpRoutes.of[IO] {
case GET -> Root / "user" :? ValidatingAge(ageResult) =>
ageResult match {
case Valid(age) if age >= 0 && age <= 150 =>
Ok(s"Valid age: $age")
case Valid(age) =>
BadRequest(s"Age out of range: $age")
case Invalid(errors) =>
BadRequest(s"Invalid age format: ${errors.head.sanitized}")
}
}Combine optional parameters with validation.
/**
* Optional validating query parameter matcher
* Returns None if parameter absent, Some(validation result) if present
*/
abstract class OptionalValidatingQueryParamDecoderMatcher[T: QueryParamDecoder](name: String) {
def unapply(params: Map[String, collection.Seq[String]]):
Some[Option[ValidatedNel[ParseFailure, T]]]
}Usage Examples:
object OptionalValidatingLimit extends OptionalValidatingQueryParamDecoderMatcher[Int]("limit")
val routes = HttpRoutes.of[IO] {
case GET -> Root / "data" :? OptionalValidatingLimit(limitOpt) =>
limitOpt match {
case None => Ok("No limit specified")
case Some(Valid(limit)) if limit > 0 => Ok(s"Limit: $limit")
case Some(Valid(limit)) => BadRequest("Limit must be positive")
case Some(Invalid(errors)) => BadRequest(s"Invalid limit: ${errors.head.sanitized}")
}
}Handle boolean flag parameters (present/absent).
/**
* Boolean flag query parameter matcher
* Returns true if parameter is present (regardless of value)
* Returns false if parameter is absent
*/
abstract class FlagQueryParamMatcher(name: String) {
def unapply(params: Map[String, collection.Seq[String]]): Option[Boolean]
}Usage Examples:
object DebugFlag extends FlagQueryParamMatcher("debug")
object VerboseFlag extends FlagQueryParamMatcher("verbose")
val routes = HttpRoutes.of[IO] {
case GET -> Root / "status" :? DebugFlag(debug) +& VerboseFlag(verbose) =>
val message = (debug, verbose) match {
case (true, true) => "Debug and verbose mode enabled"
case (true, false) => "Debug mode enabled"
case (false, true) => "Verbose mode enabled"
case (false, false) => "Normal mode"
}
Ok(message)
// ?debug -> debug=true, verbose=false
// ?debug&verbose -> debug=true, verbose=true
// (no flags) -> debug=false, verbose=false
}You can create custom decoders for complex parameter types:
// Custom case class
case class SortOrder(field: String, direction: String)
// Custom decoder
implicit val sortOrderDecoder: QueryParamDecoder[SortOrder] =
QueryParamDecoder[String].emap { str =>
str.split(":") match {
case Array(field, direction) if Set("asc", "desc").contains(direction) =>
Right(SortOrder(field, direction))
case _ =>
Left(ParseFailure("Invalid sort format, expected 'field:direction'", ""))
}
}
object SortOrderParam extends QueryParamDecoderMatcher[SortOrder]("sort")
val routes = HttpRoutes.of[IO] {
// ?sort=name:asc or ?sort=date:desc
case GET -> Root / "users" :? SortOrderParam(sort) =>
Ok(s"Sort by ${sort.field} ${sort.direction}")
}