Akka HTTP mashalling, souce-code level details
This is continued from the previous article, which explained the basics about Akka HTTP marshalling. Now we will look into how implicit
type class resolution is done, to achieve the marshalling outcome.
The complete
directive in Routing DSL
The complete
directive (method) is what we almost always use in Akka HTTP’s Routing DSL, When we want to send an HTTP response to the client.
(In Java's Akka HTTP API, there aremany variants of the complete
directive like completeWithFutureString
. However in Scala, complete
can handle a variety of different types.)
Like below, we can send a String
as the HTTP body of the response:
lazy val routes: Route = get {
complete("Hello World")
}
The client would see a response such as:
HTTP/1.1 200 OK
Server: akka-http/10.1.3
Date: Fri, 10 Aug 2018 23:33:14 GMT
Content-Type: text/plain; charset=UTF-8
Content-Length: 11
Hello World
Also, we can send a status code:
import akka.http.scaladsl.model.StatuCodes._
complete(StatusCodes.OK)
or a byte array as the HTTP body:
val data: Array[Byte] = ...
complete(data)
So far the complete
directive worked nicely with the three different types, String
, StatusCode
and Array[Byte]
, so are there a lot of overridden complete
directive to have this flexibility for the parameter?
Actually, no. There is only one definition of the directive, which is this:
def complete(m => ToResponseMarshallable)
What the hell is this ToResponseMarshallable
?
I will explain this in much more detail for the rest of the article, but in short, there is some implicit
magic going on, and this single definition can convert the parameter of all these different types to : => ToResponseMarshallable
, then marshall it to HttpResponse
or HttpRequeast
in the end.
The goal of this article is to understand what’s happening under the hood of this complete
directive, especially how implicit
resolution of type classes works, and explain why this single definition is flexible enough to accept String
, StatusCode
, and Array[Byte]
. Furthermore, if you have import
-ed or defined necessary implicit
instances for an arbitrary [T]
, Akka HTTP can marshall [T]
or even Source[T, _]
to HttpResponse
or HttpRequest
.
// if you have necessary implicit instances in scope...
import ...
implicit val ...
implicit val ...
val result: T = ...
// ...this just works, or even result: Source[T, _] works
complete(result)
The concepts: difference among Marshaller, Marshalling and Marshallble
Before diving into the code, please bear with me while I’m introducing some conceptual aspects.
The marshalling guide of the official documentation is worth reading, but it is probably overwhelming for those who are not very familiar with internals of Akka HTTP or how type classes work in general. I'll try to explain this with the least and smallest possible leap in the context so that people can follow.
To start with, conceptually there are three participant types in the Akka HTTP marshalling process, Marshallable, Marshaller and Marshalling. Don’t worry too much abut the naming, although they look very similar and feel confusing for the first time.
Marshallable represents our own type [T]
used by our application, and as the name suggests, it is something that can be marshalled to the wire format. Typically the wire format is HttpRequest
or HttpResponse
. Otherwise, once [T]
is marshalled to HttpEntity
(= HTTP body) as the intermediate step, then marshalling from [T]
to HttpRequest
or HttpResponse
is also enabled due to implicit type-class resolution. We'll see more in this implicit stuff later.
Marshaller probably feels easiest to understand among the three, as it provides the marshalling logic to convert an instance of type A
to B
. However, as in the official documentation, it is not simply A => B
:
Contrary to what you might initially expect, Marshaller[A, B] is not a plain function
A => B
but rather essentially a functionA => Future[List[Marshalling[B]]]
. Let’s dissect this rather complicated looking signature piece by piece to understand why marshallers are designed this way. Given an instance of typeA
a Marshaller[A, B] produces:
Marshaller
(source code) is defined somewhat like below, although it is not the exact but simplified definition:
// Marshalling from type A to B, simplified definition
sealed abstract class Marshaller[A, B] {
def apply(value: A)(implicit ec: ExecutionContext
): Future[List[Marshalling[B]]]
}
So the apply
method is of type A => Future[List[Marshalling[B]]]
as described in the quote from the official documentation:
- A
Future
: This is probably quite clear. Marshallers are not required to synchronously produce a result, so instead they return a future, which allows for asynchronicity in the marshalling process.- of
List
: Rather than only a single target representation forA
marshallers can offer several ones. Which one will be rendered onto the wire in the end is decided by content negotiation. For example, the Marshaller[OrderConfirmation, MessageEntity] might offer a JSON as well as an XML representation. The client can decide through the addition of an Accept request header which one is preferred. If the client doesn’t express a preference the first representation is picked.- of
Marshalling[B]
: Rather than returning an instance ofB
directly marshallers first produce aMarshalling[B]
. This allows for querying the MediaType and potentially the HttpCharset that the marshaller will produce before the actual marshalling is triggered. Apart from enabling content negotiation this design allows for delaying the actual construction of the marshalling target instance to the very last moment when it is really needed.
Finally as in the above 3., Marshalling represents “lazy” evaluation of the marshalling result.
As we have covered the conceptual side of the type classes up to this point, let's move onto the code and see how these work together.
ToResponseMarshallable
Now we can come back to the complete
directive (source code):
def complete(m: ⇒ ToResponseMarshallable): Route = ...
As explained earlier, ToResponseMarshallable
(source code) is a marshallable, representing our own application’s type [T]
, and it can be marshalled to (typically), HttpRequest
, HttpResponse
or HttpEntity
.
It is a trait
below (source code):
// simplified definition
trait ToResponseMarshallable {
type T
implicit def marshaller: ToResponseMarshaller[T]
}
Coming back to the definition of the complete
directive, it is actually defined by import
-ing ToResponseMarshallable
at the beginning of the .scala
file.
import akka.http.scaladsl.marshalling.ToResponseMarshallable
...
def complete(m: ⇒ ToResponseMarshallable): Route = ...
By this import
, the companion object of ToResponseMarshallable
(source code) is also brought into the scope of the complete
directive:
// simplified definition
object ToResponseMarshallable {
implicit def apply[T](_value:T)(implicit _marshaller: ToResponseMarshaller[T]
): ToResponseMarshallable = ...
}
The above apply
method means that a ToResponseMarshallable
, ("marshallable", something that can be marshalled) the return type of apply
, becomes available in scope, as long as there is an implicit ToResponseMarshaller[T]
("marshaller", the marshalling process) instance also available in scope.
ToResponseMarshaller
is defined as follows:
type ToResponseMarshaller[T] = Marshaller[T, HttpResponse]
One thing to note is we don’t need to import ToResponseMarshallable
from the caller of the complete
directive. To see this in action, we can look at the below simple but complete example of an Akka HTTP server application.
// no need to import ToResponseMarshallable
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
// importing akka...Directives._ makes `get` and `complete` avaialable in scope
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.stream.ActorMaterializer
object MainHelloWorld {
def main(args: Array[String]): Unit = {
// remember to make them implicit!!
implicit val system = ActorSystem("MainHelloWorld") // for Akka Actor
implicit val materializer = ActorMaterializer() // for Akka Stream
lazy val routes: Route = get { // `get` for HTTP GET method
//"Hello World": String is converted to ToResponseMarshallable
complete("Hello World")
}
Http().bindAndHandle(routes, "localhost", 8080)
}
The above code, (i.e) the caller of the complete
directive does not import ToResponseMarshallable
, however at the place we call complete("Hello World``"``)
, "Hello World": String
is converted to ToResponseMarshallable
.
Marshaller
comes into play
Once the parameter to the complete
directive is converted to ToResponseMarshallable
, then the marshaller will come into play.
As seen before, the existence of ToResponseMarshallable
means the existence of implicit ToResponseMarshaller[T]
, due to the companion object ToResponseMarshallabe
and its apply
(pasted again):
// simplified definition
object ToResponseMarshallable {
implicit def apply[T](_value:T)(implicit _marshaller: ToResponseMarshaller[T]
): ToResponseMarshallable = ...
}
If you are careful enough and familiar with type classes with implicit
, you might have noticed that we didn’t define ToResponseMarshaller[String]
ourselves. That’s correct, but it still works because Akka HTTP has pre-defined ToResponseMarshaller[String]
instance.
That’s actually achieved in two steps - firstly, there is a pre-definedimplicit ToResponseMarshaller[T]
defined (source code) :
// simplified definition
implicit def liftMarshaller[T](implicit m: ToEntityMarshaller[T]
): ToResponseMarshaller[T]
as you can see, this takes the implicit ToEntityMarshaller[T]
parameter, where ToEntityMarshaller
is defined as:
type ToEntityMarshaller[T] = Marshaller[T, MessageEntity]
// MessageEntity represents the entity of either HTTP request or response
so as long as there is an implicit ToEntityMarshaller[T]
for the entity (i.e. HTTP body) in scope, implicit ToResponseMarshaller[T]
is also available. The pre-defined marshaller for String
to the entity is as follows:
implicit val StringMarshaller: ToEntityMarshaller[String] = ...
where the trait defines ToEntityMarshaller
(source code) for other basic types too:
package akka.http.scaladsl.marshalling
trait PredefinedToEntityMarshallers extends MultipartMarshallers {
implicit val StringMarshaller: ToEntityMarshaller[String] = ...
implicit val ByteArrayMarshaller: ToEntityMarshaller[Array[Byte]] = ...
implicit val ByteStringMarshaller: ToEntityMarshaller[ByteString] = ...
implicit val CharArrayMarshaller: ToEntityMarshaller[Array[Char]] = ...
def charArrayMarshaller(mediaType: MediaType.WithOpenCharset): ToEntityMarshaller[Array[Char]] =...
...
def stringMarshaller(mediaType: MediaType.WithFixedCharset): ToEntityMarshaller[String] = ...
implicit val FormDataMarshaller: ToEntityMarshaller[FormData] = ...
implicit val MessageEntityMarshaller: ToEntityMarshaller[MessageEntity] = ...
}
Thus we got ToEntityMarshaller[String]
, and ToResponseEntity[String]
, then finally got ToResponseMarshallabe
with type T = String
in scope too, where all of these make it possible to complete("Hello World": String)
to be marshalled to the HTTP response.
The last thing I want to touch is about the packaging in Akka HTTP, which enables us to avoid unnecessary import
from the call site.
If you remember, the example Main
we saw earlier didn’t need to import ToResponseMarshallable
nor import
pre-defined marshallers. That’s because the pre-defined marshallers are made available to ToResponseMarshaller
as they are all in the same package, package akka.http.scaladsl.marshalling
. So for the types with pre-defined marshallers, we don’t need to define their implicit
marshller instances nor import
anything.
spray-json
For a type T
without pre-defined marshallers, you need to define or import
necessary implicit
marshaller instances for T
. More specifically, ToEntityMarshaller
is needed, then ToEntityMarshaller[T]
to ToResponseMarshaller[T]
is done by the pre-defined implicit
as discussed in the previous section.
Specifically for spray-json, since it already defines the ToEntityMarshaller
:
implicit def sprayJsonMarshaller[T](
implicit writer: RootJsonWriter[T],
printer: JsonPrinter = CompactPrinter
): ToEntityMarshaller[T]
instead of defining ToEntityMarshaller
directly, we can just define RootJsonWriter[T]
, where convenience methods like jsonFormat2
are provided.
import spray.json.DefaultJsonProtocol._
import spray.json.RootJsonFormat
case class User(name: String, age: Int)
object User {
implicit val userJsonFormat: RootJsonFormat[User] = jsonFormat2(User.apply)
}
One thing extra, the headers
Talking about marshalling to the HTTP response, you might have been wondering what’s going on with the headers. The headers are indeed set by marshallers, and the default is defined as follows:
def fromToEntityMarshaller[T](
status: StatusCode = StatusCodes.OK,
headers: immutable.Seq[HttpHeader] = Nil
)(implicit m: ToEntityMarshaller[T]
): ToResponseMarshaller[T] =
fromStatusCodeAndHeadersAndValue compose (t ⇒ (status, headers, t))
which is called from the earlier **implicit def**
liftMarshaller[T]
. Thus, by default headers: immutable.Seq[HttpHeader] =
*Nil*
so there’s only minimal headers are stamped like below:
HTTP/1.1 200 OK
Server: akka-http/10.1.3
Date: Sat, 11 Aug 2018 00:17:41 GMT
Content-Type: text/plain; charset=UTF-8
Content-Length: 11
Summary
Here, I have gone through how the implicit
resolution of marshalling-related type classes happens step by step. I hope this gave you a better understanding of marshalling. Thanks for being patient with this long article with lots of text and code, without any visualization unlike my other blog posts.