Skip to content

PureConfig Derivation

JVM-only module. Drop-in replacement for pureconfig-generic / pureconfig-generic-scala3 -- derives ConfigReader, ConfigWriter, and ConfigConvert for case classes, sealed traits, Scala 3 enums, and more.

Need cross-platform HOCON?

If you need HOCON support on Scala.js or Scala Native, use kindlings-sconfig-derivation instead — it provides the same configuration API built on sconfig, a cross-platform port of Typesafe Config.

Installation

sbt

libraryDependencies += "com.kubuszok" %% "kindlings-pureconfig-derivation" % "0.2.0"

PureConfig derivation is JVM-only (no %%%). The PureConfig runtime (com.github.pureconfig:pureconfig-core) is pulled in transitively.

Scala CLI

//> using dep com.kubuszok::kindlings-pureconfig-derivation:0.2.0

Quick start

Reading and writing HOCON configuration
//> using dep com.kubuszok::kindlings-pureconfig-derivation:0.2.0

import hearth.kindlings.pureconfigderivation._
import pureconfig._

case class ServerConfig(host: String, port: Int, debug: Boolean)

// Semi-automatic derivation
val reader: ConfigReader[ServerConfig] = KindlingsConfigReader.derived[ServerConfig]
val writer: ConfigWriter[ServerConfig] = KindlingsConfigWriter.derived[ServerConfig]

// Read from HOCON string
val result = ConfigSource.string("""
  host = "localhost"
  port = 8080
  debug = false
""").load[ServerConfig](reader)
println(result)
// expected output:
// Right(ServerConfig(localhost,8080,false))

API

Derivation methods

Method Returns Description
KindlingsConfigReader.derived[A] ConfigReader[A] Semi-automatic reader
KindlingsConfigReader.derived[A] KindlingsConfigReader[A] Sanely-automatic (given/implicit)
KindlingsConfigWriter.derived[A] ConfigWriter[A] Semi-automatic writer
KindlingsConfigWriter.derived[A] KindlingsConfigWriter[A] Sanely-automatic (given/implicit)
KindlingsConfigConvert.derived[A] ConfigConvert[A] Semi-automatic reader + writer
KindlingsConfigConvert.derived[A] KindlingsConfigConvert[A] Sanely-automatic reader + writer

All methods take an implicit/using PureConfig parameter (defaults to PureConfig.default).

Type hierarchy

KindlingsConfigReader[A] extends pureconfig.ConfigReader[A], KindlingsConfigWriter[A] extends pureconfig.ConfigWriter[A], and KindlingsConfigConvert[A] extends pureconfig.ConfigConvert[A]. Derived instances are fully drop-in compatible with existing PureConfig code, including ConfigSource.load[A].

Configuration

All derivation methods accept an implicit PureConfig:

import hearth.kindlings.pureconfigderivation._

implicit val config: PureConfig = PureConfig.default
  .withSnakeCaseMemberNames
  .withDiscriminator("type")
  .withUseDefaults

The defaults match upstream PureConfig behavior: kebab-case member names, kebab-case constructor names, "type" discriminator, defaults enabled, and unknown keys allowed.

Builder method Description
withSnakeCaseMemberNames fieldName -> field_name
withKebabCaseMemberNames fieldName -> field-name (default)
withPascalCaseMemberNames fieldName -> FieldName
withScreamingSnakeCaseMemberNames fieldName -> FIELD_NAME
withCamelCaseMemberNames fieldName -> fieldName (identity)
withTransformMemberNames(f) Custom field name transform
withSnakeCaseConstructorNames MyType -> my_type in discriminator
withKebabCaseConstructorNames MyType -> my-type in discriminator (default)
withTransformConstructorNames(f) Custom constructor name transform
withDiscriminator(field) ADT discriminator field name (default: "type")
withWrappedSubtypes No discriminator -- wrap subtypes in single-key objects
withUseDefaults Use case class default values for missing keys (default)
withoutUseDefaults Require all keys
withAllowUnknownKeys Ignore unexpected HOCON keys (default)
withStrictDecoding Fail on unexpected HOCON keys

Per-type hints

For per-type customization (e.g. one case class uses snake_case while everything else stays kebab-case), define an implicit KindlingsProductHint or KindlingsCoproductHint for the specific type. The macro looks them up at derivation time and falls back to the global PureConfig when no per-type hint is in scope.

Annotations

Annotation Description
@configKey("name") Override the HOCON key for a case class field
@transientField Exclude a field from reading/writing (must have a default value)
import hearth.kindlings.pureconfigderivation.annotations._

case class DatabaseConfig(
  @configKey("connection_string") connectionString: String,
  @transientField cachedPool: Option[Any] = None
)

Usage examples

Sealed trait with discriminator
//> using dep com.kubuszok::kindlings-pureconfig-derivation:0.2.0

import hearth.kindlings.pureconfigderivation._
import pureconfig._

sealed trait DatabaseType
case class Postgres(host: String, port: Int, database: String) extends DatabaseType
case class Sqlite(path: String) extends DatabaseType

// Default config uses "type" discriminator and kebab-case names
val reader = KindlingsConfigReader.derived[DatabaseType]

val result = ConfigSource.string("""
  type = "postgres"
  host = "localhost"
  port = 5432
  database = "mydb"
""").load[DatabaseType](reader)
println(result)
// expected output:
// Right(Postgres(localhost,5432,mydb))
Wrapped subtypes (no discriminator)
//> using dep com.kubuszok::kindlings-pureconfig-derivation:0.2.0

import hearth.kindlings.pureconfigderivation._
import pureconfig._

sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape

implicit val config: PureConfig = PureConfig.default.withWrappedSubtypes

val reader = KindlingsConfigReader.derived[Shape]

val result = ConfigSource.string("""
  circle { radius = 5.0 }
""").load[Shape](reader)
println(result)
// expected output:
// Right(Circle(5.0))
Case class with defaults and strict decoding
//> using dep com.kubuszok::kindlings-pureconfig-derivation:0.2.0

import hearth.kindlings.pureconfigderivation._
import pureconfig._

implicit val config: PureConfig = PureConfig.default
  .withUseDefaults
  .withStrictDecoding

case class AppSettings(
  host: String,
  port: Int = 8080,
  debug: Boolean = false
)

val reader = KindlingsConfigReader.derived[AppSettings]

// Missing fields use Scala defaults
val result = ConfigSource.string("""host = "localhost" """).load[AppSettings](reader)
println(result)
// expected output:
// Right(AppSettings(localhost,8080,false))

// Unknown keys cause an error in strict mode
val strict = ConfigSource.string("""
  host = "localhost"
  unknown-key = "oops"
""").load[AppSettings](reader)
println(strict.isLeft)
// expected output:
// true
Snake case member names
//> using dep com.kubuszok::kindlings-pureconfig-derivation:0.2.0

import hearth.kindlings.pureconfigderivation._
import pureconfig._

implicit val config: PureConfig = PureConfig.default
  .withSnakeCaseMemberNames

case class UserProfile(firstName: String, lastName: String, emailAddress: String)

val reader = KindlingsConfigReader.derived[UserProfile]

val result = ConfigSource.string("""
  first_name = "Alice"
  last_name = "Smith"
  email_address = "alice@example.com"
""").load[UserProfile](reader)
println(result)
// expected output:
// Right(UserProfile(Alice,Smith,alice@example.com))
Nested configuration
//> using dep com.kubuszok::kindlings-pureconfig-derivation:0.2.0

import hearth.kindlings.pureconfigderivation._
import pureconfig._

case class HttpConfig(host: String, port: Int)
case class DatabaseConfig(url: String, pool: Int)
case class AppConfig(http: HttpConfig, database: DatabaseConfig)

val reader = KindlingsConfigReader.derived[AppConfig]

val result = ConfigSource.string("""
  http {
    host = "0.0.0.0"
    port = 8080
  }
  database {
    url = "jdbc:postgresql://localhost/mydb"
    pool = 10
  }
""").load[AppConfig](reader)
println(result)
// expected output:
// Right(AppConfig(HttpConfig(0.0.0.0,8080),DatabaseConfig(jdbc:postgresql://localhost/mydb,10)))

Debugging

Import the debug package to log the derivation process at compile time:

import hearth.kindlings.pureconfigderivation.debug._

This enables LogDerivation implicits for KindlingsConfigReader, KindlingsConfigWriter, and KindlingsConfigConvert, printing the derivation steps to the compiler output.

Comparison with pureconfig-generic

Feature differences

Feature pureconfig-generic (Scala 2) pureconfig-generic-scala3 Kindlings
Same API on Scala 2.13 and 3 No No Yes
Sanely-automatic derivation No No Yes
derives support on Scala 2.13 No N/A Yes
Recursive types Needs Lazy Yes Just works
Named tuples No No Yes
Opaque types No Partial Yes
Scala 3 enums No Yes Yes
Java enums No Yes Yes
Per-type ProductHint / CoproductHint Yes Yes Yes
@ConfiguredJsonCodec-style annotation No No No

Benchmarks

All values in ops/s (higher is better). Measured on macOS, JVM temurin 17.

Write

Type Scala Kindlings Original semi vs original
SimpleCC 2.13 10.5M 1.4M 7.6x faster
SimpleCC 3 11.4M 1.1M 10x faster
Person 2.13 1.1M 218.1K 5.1x faster
Person 3 1.2M 207.3K 5.5x faster

Read

Type Scala Kindlings Original semi vs original
SimpleCC 2.13 15.6M 1.4M 11x faster
SimpleCC 3 17.3M 959.7K 18x faster
Person 2.13 783.5K 214.2K 3.7x faster
Person 3 760.1K 173.6K 4.4x faster

Note

Kindlings is 3.7--18x faster across the board, for both reads and writes, on both Scala versions and type complexities.

Note: Kindlings semi-automatic and automatic derivation produce identical performance -- this is the "sanely-automatic" design.