Case Class Extraction

The value extraction performed by Directives is a nice way of providing your route logic with interesting request properties, all with proper type-safety and error handling. However, in some case you might want even more. Consider this example:

case class Color(red: Int, green: Int, blue: Int)

val route =
  path("color") {
    parameters('red.as[Int], 'green.as[Int], 'blue.as[Int]) { (red, green, blue) =>
      val color = Color(red, green, blue)
      // ... route working with the `color` instance
      null // hide
    }
  }
Get("/color?red=1&green=2&blue=3") ~> route ~> check { responseAs[String] shouldEqual "Color(1,2,3)" } // hide

Here the parameters-scala directives is employed to extract three Int values, which are then used to construct an instance of the Color case class. So far so good. However, if the model classes we’d like to work with have more than just a few parameters the overhead introduced by capturing the arguments as extractions only to feed them into the model class constructor directly afterwards can somewhat clutter up your route definitions.

If your model classes are case classes, as in our example, Akka HTTP supports an even shorter and more concise syntax. You can also write the example above like this:

case class Color(red: Int, green: Int, blue: Int)

val route =
  path("color") {
    parameters('red.as[Int], 'green.as[Int], 'blue.as[Int]).as(Color) { color =>
      // ... route working with the `color` instance
      null // hide
    }
  }
Get("/color?red=1&green=2&blue=3") ~> route ~> check { responseAs[String] shouldEqual "Color(1,2,3)" } // hide

You can postfix any directive with extractions with an as(...) call. By simply passing the companion object of your model case class to the as modifier method the underlying directive is transformed into an equivalent one, which extracts only one value of the type of your model class. Note that there is no reflection involved and your case class does not have to implement any special interfaces. The only requirement is that the directive you attach the as call to produces the right number of extractions, with the right types and in the right order.

If you’d like to construct a case class instance from extractions produced by several directives you can first join the directives with the & operator before using the as call:

case class Color(name: String, red: Int, green: Int, blue: Int)

val route =
  (path("color" / Segment) & parameters('r.as[Int], 'g.as[Int], 'b.as[Int]))
    .as(Color) { color =>
      // ... route working with the `color` instance
      null // hide
    }
Get("/color/abc?r=1&g=2&b=3") ~> route ~> check { responseAs[String] shouldEqual "Color(abc,1,2,3)" } // hide

Here the Color class has gotten another member, name, which is supplied not as a parameter but as a path element. By joining the path and parameters directives with & you create a directive extracting 4 values, which directly fit the member list of the Color case class. Therefore you can use the as modifier to convert the directive into one extracting only a single Color instance.

Generally, when you have routes that work with, say, more than 3 extractions it’s a good idea to introduce a case class for these and resort to case class extraction. Especially since it supports another nice feature: validation.

Caution

There is one quirk to look out for when using case class extraction: If you create an explicit companion object for your case class, no matter whether you actually add any members to it or not, the syntax presented above will not (quite) work anymore. Instead of as(Color) you will then have to say as(Color.apply). This behavior appears as if it’s not really intended, so this might be improved in future Scala versions.

Case Class Validation

In many cases your web service needs to verify input parameters according to some logic before actually working with them. E.g. in the example above the restriction might be that all color component values must be between 0 and 255. You could get this done with a few validate directives but this would quickly become cumbersome and hard to read.

If you use case class extraction you can put the verification logic into the constructor of your case class, where it should be:

case class Color(name: String, red: Int, green: Int, blue: Int) {
  require(!name.isEmpty, "color name must not be empty")
  require(0 <= red && red <= 255, "red color component must be between 0 and 255")
  require(0 <= green && green <= 255, "green color component must be between 0 and 255")
  require(0 <= blue && blue <= 255, "blue color component must be between 0 and 255")
}

If you write your validations like this Akka HTTP’s case class extraction logic will properly pick up all error messages and generate a ValidationRejection if something goes wrong. By default, ValidationRejections are converted into 400 Bad Request error response by the default RejectionHandler, if no subsequent route successfully handles the request.