Scala macro annotations: a real-world example

Martin Raison
Keep It Up
Published in
4 min readJul 15, 2014

TL;DR: github.com/kifi/json-annotation. Also, sign up for Kifi.

Scala macros can be a great tool to reduce boilerplate code. For example, Play provides macros for easily creating Json serializers. Given a case class, it is easy to define a serializer inside the companion object:

Here, Json.format[A] is a macro that will transform the definition of format at compile time into the following full-fledged serializer:

This implicit format can then be used by the methods Json.toJson and Json.fromJson to serialize/deserialize instances of class A.

While this already greatly simplifies the creation of serializers, here are two small caveats:

  1. In our production code for Kifi, we have many different data models represented by case classes. In most cases we also want a Json format, so every time we need to create a companion object whose sole purpose is to contain the format definition. Can we make that even simpler?
  2. Sometimes the case class has only one field, i.e. it is just a wrapper around another type such as String or Int. For example:

In that case, Play’s default serialization will be {“value”: 34}, which is fine but we’d rather have just 34, since it’s shorter and there is no ambiguity on the field. So we need to come up with our own format, which is something like:

It would be nice if this could be simplified.

What should we do?

It turns out these two problems can be elegantly solved with a single Scala macro annotation. Macro annotations are a new experimental feature of Scala macros, only available in macro paradise for the moment. Before we go through any technical details, here is what the result looks like:

That’s it! No need to explicitly create a companion object, and if the case class has only one field, we will get our “short” serializer instead of Play’s default one. The only thing we need to do is to add the @json annotation in front of the class definition. How did that magic happen?

During compilation, the @json annotation triggers the execution of a macro on the class definition. This way the class definition can be modified as desired. Even better, this also allows the companion object to be modified, or created if it doesn’t already exist. Here’s how the definition of the annotation looks:

The annotation is created by extending StaticAnnotation, and providing a macro implementation for the macroTransform method (using the keyword macro). The implementation itself inspects the annottees (i.e. the syntax tree nodes corresponding to the code under the annotation). It extracts the class declaration (classDecl), and also the companion object declaration (compDecl) if available.

The helper method modifiedClass is where the black magic takes place. The goal is to create an Abstract Syntax Tree (= a piece of code, but in a form that the compiler understands) corresponding to the class + companion object declaration that you want to produce. Without going too much into the specifics, here are the main steps of the transformation (the complete source code can be found here):

  • Use unlifting to extract the name of the case class and its fields (we need to know how many fields the case class has in order to generate the correct Json formatter):

Note the use of c.abort for aborting the execution of the macro with an error message.

  • Generate the appropriate json format. We’re using lifting again, but this time it’s the other way around since we’re putting things in quasiquotes instead of extracting them:
  • Combine unlifting and lifting to insert the format in the companion object if it already exists:

A more detailed explanation of unquoting (lifting and unlifting) is available in the documentation.

Finally, remember that using macros requires compilation to happen in two steps: first, compile the macros, then compile the code where the macros are used. This is necessary so that your macros can be run before the rest of your code is compiled. For example if you use SBT, you can configure Build.scala to use two modules, a “macros” module containing your macros, and a “root” module that depends on the “macros” module.

Sounds great, can I use it?

Yes! But keep in mind that macro annotations are still experimental. If you use Play (version 2.1 or higher) you should just need to add the following two settings to your build configuration:

Then import com.kifi.macros.json whenever you want to use the @json annotation. More information can be found at github.com/kifi/json-annotation along with the source code, in case you want to start writing your own macro annotations. Happy hacking!

We wrote this post while working on Kifi —Connecting People with Knowledge. Learn more.

Originally published at eng.kifi.com on July 15, 2014.

--

--