Derivation Policy
By default Kindlings derivation is always allowed: wherever an instance of a Kindlings type class is needed, the macro will derive one (recursively, in a single expansion). This is the sanely-automatic behavior, and for most projects it is exactly what you want.
Some teams, however, want tighter control over where instances come into existence — for example, to ensure every canonical codec is defined in one designated place rather than materialized ad-hoc at a random call site. The derivation policy provides this without introducing a separate "semi-automatic" API or making recursion optional (see Why not separate automatic and semi-automatic derivation?).
The policy is a compile-time, per-library switch configured entirely through -Xmacro-settings. It gates only
structural derivation — generating an instance for your case classes and sealed hierarchies / enums.
Everything else keeps working unconditionally:
- a type that already has an instance in implicit scope (your hand-written
given/implicit), - built-in/primitive support, collections,
Option, value types / newtypes, named tuples.
In other words, the policy never blocks "the parts that always work" — it only decides whether the macro is allowed to structurally derive a new instance for a product/sum type at this location.
Modes
<namespace>.policy.enabled takes one of:
| Value | Behavior |
|---|---|
always-allowed (default) |
Current behavior — structural derivation is permitted everywhere. |
opt-in |
Structural derivation is permitted only in allowed scopes, or behind an opt-in import (see below). Everywhere else it is a compile error with an actionable message. |
The <namespace> is per library (the same namespace already used for the derivation timeout).
Each library is configured independently, so you can lock down (say) your JSON codecs while leaving Show/Eq
derivation wide open.
Configuration
# Turn the policy on for a library (here: FastShowPretty):
-Xmacro-settings:fastShowPrettyDerivation.policy.enabled=opt-in
# Allow structural derivation in these scopes (packages, objects, or classes — by fully-qualified name).
# A package entry covers everything nested under it; an object/class entry covers its inner derivations.
-Xmacro-settings:fastShowPrettyDerivation.policy.allowedScopes=com.acme.json;com.acme.Codecs
# Whether importing the opt-in marker also permits derivation in the importing scope (default: true).
-Xmacro-settings:fastShowPrettyDerivation.policy.optInByImport=true
Separate allowedScopes entries with ; or |, never ,
A comma is not portable across compilers: Scala 3 splits a single -Xmacro-settings:a,b option on commas into
two settings, while Scala 2 keeps it as one string. Kindlings therefore splits allowedScopes on ; or |, which
neither compiler touches. (If you prefer, you can also pass several separate -Xmacro-settings: options.)
In sbt:
Compile / scalacOptions ++= Seq(
"-Xmacro-settings:fastShowPrettyDerivation.policy.enabled=opt-in",
"-Xmacro-settings:fastShowPrettyDerivation.policy.allowedScopes=com.acme.json;com.acme.Codecs"
)
How a location is decided
When the macro is about to structurally derive an instance, it checks, once per expansion:
- If
enabledisalways-allowed(the default) → allowed. - Otherwise (
opt-in): if any enclosing scope (package / object / class) of the expansion point matches anallowedScopesentry → allowed. - Otherwise, if
optInByImport=trueand the library's opt-in marker is in implicit scope → allowed. - Otherwise → compile error, explaining the current location, the configured scopes, and every way to fix it.
Because a derivation is one macro expansion at one call site, this single decision covers the whole recursive derivation: if the location is allowed, all nested product/sum types derive too (you define the instance once, in an allowed place); if it is denied, the macro fails fast before generating anything.
Opt-in by import
When optInByImport=true, importing the library's marker permits derivation in the current scope even if it is not on
allowedScopes:
import hearth.kindlings.fastshowpretty.policy.allowDerivationForFastShowPretty
// now FastShowPretty.derived[...] is allowed in this scope
Set optInByImport=false if you want allowedScopes to be the only way in.
Worked example
Build configuration (opt-in, one allowed package):
-Xmacro-settings:fastShowPrettyDerivation.policy.enabled=opt-in
-Xmacro-settings:fastShowPrettyDerivation.policy.allowedScopes=com.acme.show
package com.acme.show
import hearth.kindlings.fastshowpretty.FastShowPretty
final case class User(id: Long, name: String)
object Instances {
// ✓ Allowed: this object is under the allowed package `com.acme.show`.
given FastShowPretty[User] = FastShowPretty.derived
}
package com.acme.web
import hearth.kindlings.fastshowpretty.FastShowPretty
import com.acme.show.User
object Handler {
// ✗ Compile error: `com.acme.web` is not an allowed scope and the opt-in marker is not imported.
// val show = FastShowPretty.derived[User]
// ✓ Use the canonical instance defined in the allowed scope instead:
import com.acme.show.Instances.given
def render(u: User): String = summon[FastShowPretty[User]].render(new StringBuilder, /*config*/ ???, 0)(u).toString
}
The error you would get from the commented-out line looks like:
Derivation of FastShowPretty[com.acme.show.User] is enabled only in the following scopes:
- com.acme.show
Currently you are in the following scope: com.acme.web.Handler.
You are allowed to enable this derivation locally by adding the import:
import hearth.kindlings.fastshowpretty.policy.allowDerivationForFastShowPretty
When opt-in is set with no allowedScopes, derivation is globally disabled and the message is instead:
Derivation of FastShowPretty[com.acme.show.User] is globally disabled.
You are allowed to enable this derivation locally by adding the import:
import hearth.kindlings.fastshowpretty.policy.allowDerivationForFastShowPretty
(The import line is appended only when optInByImport is enabled.)
Per-library namespaces
The policy keys live under each library's settings namespace — the same one used for its
derivation timeout. The opt-in marker is imported from that library's policy package.
| Library | Namespace | Opt-in marker import (hearth.kindlings.…) |
|---|---|---|
| FastShowPretty | fastShowPrettyDerivation |
fastshowpretty.policy.allowDerivationForFastShowPretty |
| Circe | circeDerivation |
circederivation.policy.allowDerivationForCirceDerivation |
| Jsoniter Scala | jsoniterDerivation |
jsoniterderivation.policy.allowDerivationForJsoniterDerivation |
| UBJson | ubjsonDerivation |
ubjsonderivation.policy.allowDerivationForUbjsonDerivation |
| Diff | diffDerivation |
diffderivation.policy.allowDerivationForDiffDerivation |
| YAML | yamlDerivation |
yamlderivation.policy.allowDerivationForYamlDerivation |
| XML | xmlDerivation |
xmlderivation.policy.allowDerivationForXmlDerivation |
| sconfig | sconfigDerivation |
sconfigderivation.policy.allowDerivationForSconfigDerivation |
| PureConfig | pureconfigDerivation |
pureconfigderivation.policy.allowDerivationForPureconfigDerivation |
| Avro | avroDerivation |
avroderivation.policy.allowDerivationForAvroDerivation |
| Tapir Schema | tapirSchemaDerivation |
tapirschemaderivation.policy.allowDerivationForTapirSchemaDerivation |
| ScalaCheck | scalacheckDerivation |
scalacheckderivation.policy.allowDerivationForScalacheckDerivation |
| Cats | catsDerivation |
catsderivation.policy.allowDerivationForCatsDerivation |
| Cats Tagless | catsTaglessDerivation |
catstaglessderivation.policy.allowDerivationForCatsTaglessDerivation |
All derivation modules use the identical <namespace>.policy.* keys (enabled, allowedScopes, optInByImport) and
expose their opt-in marker from the module's policy package. Each library is configured independently — locking down
your JSON codecs does not affect Show/Eq derivation, and vice versa.