0

After reading the usage of scala.annotation.meta.field https://www.scala-lang.org/api/current/scala/annotation/meta/index.html

Since I am using Xstream I am currently in need of setting the annotation @XStreamAlias("alias") for the field to change how they will be deserialised. For simplicity, I use a type alias:

import com.thoughtworks.xstream.annotations.XStreamAlias

type xStreamAlias = XStreamAlias @field

@XStreamAlias("a")
case class A(@xStreamAlias("B") b: String)

I am using this in around 40 classes so I used a package object also for simplicity.

However I now would like to switch to Jackson and retain one single annotation that includes both Jackson and Xstream both for compatibility and simplicity. I would like to do something similar to this:

import com.fasterxml.jackson.annotation.JsonProperty
import com.thoughtworks.xstream.annotations.XStreamAlias

type alias = XStreamAlias JsonProperty @field

@alias("a")
case class A(@alias("B") b: String)

This way I hope to combine both Jackson and XStream into a single annotation, since they will be used in the same way.

My questions are:

  1. Does this usage look correct and maintainable?
  2. If you see the examples above, I am using such annotations only for fields and not for classes. Can the annotation be safely used even for classes?

I have never seen such usage of mixing 2 annotations with a type alias and to me it looks like as an improvement to maintainability and readability to centralise the behaviour of the chosen annotation.

I already saw this question: Scala Type aliases for annotations but I am not sure if there is a new way to do so and the users that answered did not seem to be sure about their solutions.

Jac
  • 531
  • 1
  • 4
  • 19

1 Answers1

2

The right hand side of type xStreamAlias = XStreamAlias @field is the type XStreamAlias annotated with @field. So type alias = XStreamAlias JsonProperty @field can't be correct syntax.

You can define a macro annotation that being expanded switches both @XStreamAlias and @JsonProperty on with proper (the same) parameters. You can define how to handle cases when @alias annotates a field and class separately.

import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

@compileTimeOnly("Enable macro annotations")
class alias(name: String) extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro AliasMacro.impl
}

object AliasMacro {
  def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
    import c.universe._

    val name = c.prefix.tree match {
      case q"new alias(${nme: String})" => nme        
    }

    val XStreamAlias = tq"_root_.com.thoughtworks.xstream.annotations.XStreamAlias"
    val JsonProperty = tq"_root_.com.fasterxml.jackson.annotation.JsonProperty"
    val fieldAnnotation = tq"_root_.scala.annotation.meta.field"

    annottees match {
      // @alias annotates field
      case q"$_ val $valTName: $_ = $_" ::
        q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" ::
        objectOrNil =>

        // switch both @XStreamAlias and @JsonProperty on
        val paramss1 = paramss.map(_.map {
          case q"$paramMods val $paramTName: $paramTpt = $paramExpr" if paramTName == valTName =>
            val paramMods1 = paramMods.mapAnnotations(annotations =>
              q"new ($XStreamAlias @$fieldAnnotation)($name)" ::
                q"new ($JsonProperty @$fieldAnnotation)($name)" ::
                annotations
            )
            q"$paramMods1 val $paramTName: $paramTpt = $paramExpr"

          case param => param
        })

        q"""
          $mods class $tpname[..$tparams] $ctorMods(...$paramss1) extends { ..$earlydefns } with ..$parents { $self => ..$stats }
          ..$objectOrNil
        """
          
      // @alias annotates class
      case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" ::
        objectOrNil =>

        // switch only @XStreamAlias on
        val mods1 = mods.mapAnnotations(annotations =>
          q"new $XStreamAlias($name)" :: annotations
        )

        q"""
          $mods1 class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }
          ..$objectOrNil
        """
    }
  }
}

Usage:

@alias("a")
case class A(b0: Int, @alias("B") b: String, b1: Boolean)

//scalac: {
//  @new _root_.com.thoughtworks.xstream.annotations.XStreamAlias("a") case class A extends scala.Product with scala.Serializable {
//    <caseaccessor> <paramaccessor> val b0: Int = _;
//    @new _root_.com.thoughtworks.xstream.annotations.XStreamAlias @_root_.scala.annotation.meta.field("B") @new _root_.com.fasterxml.jackson.annotation.JsonProperty @_root_.scala.annotation.meta.field("B") <caseaccessor> <paramaccessor> val b: String = _;
//    <caseaccessor> <paramaccessor> val b1: Boolean = _;
//    def <init>(b0: Int, @new _root_.com.thoughtworks.xstream.annotations.XStreamAlias @_root_.scala.annotation.meta.field("B") @new _root_.com.fasterxml.jackson.annotation.JsonProperty @_root_.scala.annotation.meta.field("B") b: String, b1: Boolean) = {
//      super.<init>();
//      ()
//    }
//  };
//  ()
//}
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Nice usage, thanks a lot for the descriptive example also! This will probably require some rewriting if used with Scala 3 right? – Jac Nov 17 '20 at 14:50
  • 1
    @Jac Yeah, in Scala 3 currently macro annotations do not work. You can use [Scalameta](https://scalameta.org/)/[Scalafix](https://scalacenter.github.io/scalafix/) to expand annotations at pre-compile-time. https://scalacenter.github.io/scala-3-migration-guide/docs/macros/macro-libraries.html#macro-annotations-libraries Please notice that there is Simulacrum library for Scala 2 (compile time) and Simulacrum-Scalafix for Scala 3 (pre-compile-time). – Dmytro Mitin Nov 17 '20 at 14:55
  • Thanks! Really helpful info! – Jac Nov 17 '20 at 15:03