Prevents leaking sensitive fields defined inside
case class.
In Scala, case class(es) are omnipresent: they are the building blocks for complex business domain models, due to how
easily they can be defined and instantiated; on top of that, the Scala compiler provides a convenient toString method
for us that will pretty print in console/log their content, for example:
case class UserPreferences(useDarkTheme: Boolean, maxHistoryItems: Int)
val id = 123
val up = store.getUserPreferencesByID(id)
log.info(s"user preferences for user $id are $up")will print
user preferences for user 123 are UserPreferences(true, 5)
However, this becomes a double-edge sword when handling sensitive data: assume you're writing an HTTP server, and you have a case class to pass its headers around, i.e.
case class HttpHeaders(userId: String, apiKey: String, languages: Seq[Locale], correlationId: String)or a case class representing a user in a DB
case class User(id: UUID, nickname: String, email: String)you probably wouldn't want to leak by mistake an apiKey (for security reasons) or an email (for PII/GDPR reasons).
Sure, you can get creative and define middleware layers/utility methods and so on to circumvent the issue, but wouldn't it be better if you were simply to say "when I dump the whole object, I don't want this field to be printed out" ?
@redacted to the rescue!
This Readme is being intentionally kept short; if you'd like to learn more about advanced usecases of the annotation, or how the compiler plugin itself is structured and works, feel free to head over the project's website https://polentino.github.io/redacted/ :)
In your project/plugins.sbt add the following line
addSbtPlugin("io.github.polentino" % "sbt-redacted" % "1.3.0")and then enable it in your specific (sub)project in build.sbt like so
lazy val root = (project in file("."))
.enablePlugins(RedactedPlugin)
.setting(
redactedVersion := "0.10.0"
)Once configured your project, all you have to do is the following
import io.github.polentino.redacted.redacted
case class HttpHeaders(userId: UUID, @redacted apiKey: String, languages: Seq[Locale], correlationId: String)
case class User(id: UUID, nickname: String, @redacted email: String)That's all!
From now on, every time you'll try to dump the whole object,or invoke toString method
val headers: HttpHeaders = HttpHeaders(
userId = UUID.randomUUID(),
apiKey = "abcdefghijklmnopqrstuvwxyz",
languages = Seq("it_IT", "en_US"),
correlationId = "corr-id-123"
)
val user: User = User(
id = UUID.randomUUID(),
nickname = "polentino911",
email = "[email protected]"
)
println(headers)
println(user)this will actually be printed
$ HttpHeaders(d58b6a78-5411-4bd4-a0d3-e1ed38b579c4, ***, Seq(it_IT, en_US), corr-id-123)
$ User(8b2d4570-d043-473b-a56d-fe98105ccc2b, polentino911, ***)
But, of course, accessing the field itself will return its content, i.e.
println(headers.apiKey)
println(user.email)will still print the real values:
$ abcdefghijklmnopqrstuvwxyz
$ [email protected]
The redacted ecosystem (annotation library, compiler plugin, sbt plugin) are all compiled on Java 17; therefore,
you need to make sure Java 17+ is available in your system. Support for specific Scala versions is listed in the
table below. However, it is advised to use the Long Term Support ones (as listed in the Scala website).
| Scala Version | JVM | Native (0.5) | JS (1.x) | Notes |
|---|---|---|---|---|
| 3.9.0 | ✅ | ✅ | ✅ | LTS |
| 3.8.4 | ✅ | - | - | - |
| 3.7.4 | ✅ | ✅ | ✅ | - |
| 3.6.4 | ✅ | - | - | - |
| 3.5.2 | ✅ | - | - | - |
| 3.4.3 | ✅ | - | - | - |
| 3.3.8 | ✅ | ✅ | ✅ | LTS |
| 3.3.7 | ✅ | - | - | LTS |
| 3.3.6 | ✅ | - | - | LTS |
| 3.3.5 | ✅ | - | - | LTS |
| 3.3.4 | ✅ | - | - | LTS |
| 3.3.3 | ✅ | - | - | LTS |
| 3.3.1 | ✅ | - | - | LTS |
| 3.3.0 | ✅ | - | - | LTS |
| 3.2.2 | ✅ | - | - | - |
| 3.1.3 | ✅ | - | - | - |
| 2.13.18 | ✅ | ✅ | ✅ | - |
| 2.12.21 | ✅ | ✅ | ✅ | - |
redacted runs on the JVM, Scala.js and Scala Native. The
compiler plugin only rewrites toString at compile time and is never shipped in the final binary, so it stays
JVM-only; the @redacted annotation library is cross-published. In JS/Native projects depend on the library with
%%% and keep the plugin on %%:
libraryDependencies += "io.github.polentino" %%% "redacted" % redactedVersion cross CrossVersion.full
addCompilerPlugin("io.github.polentino" %% "redacted-plugin" % redactedVersion cross CrossVersion.full)Given a case class with at least one field annotated with @redacted, i.e.
final case class User(id: UUID, @redacted name: String)the compiler plugin will replace the default implementation of its toString method with this
final case class User(id: UUID, @redacted name: String) {
def toString(): String = "User(" + this.id + ",***" + ")"
}The way it's done is the following:
The compiler plugin will inspect each type definition and check whether the class being analysed is a case class, and
if it has at least one of its fields annotated with @redacted ; if that's the case, it will then proceed to rewrite
the default toString implementation by selectively returning either the *** string, or the value of the field,
depending on the presence (or not) of @redacted, resulting in an implementation that looks like so:
def toString(): String =
"<class name>(" + this.< field not redacted > + "," + "***" + ... +")"- create Sbt plugin (https://github.com/polentino/sbt-redacted)
- add some benchmarks with jmh
- Awesome pointers and ideas by Kit Langton (although it's about macros and not compiler plugins)
- Compiler Plugin Development in Scala 3 | Let's talk about Scala 3
- act
Here below you'll find a list of companies using redacted in their product(s)
Want to see your company here? Fork the repo and submit a PR!
- InvestSuite - B2B, Scalable & AI-Driven WealthTech

