Skip to content

Circe Derivation

Drop-in replacement for circe-generic / circe-generic-extras — derives Encoder, Encoder.AsObject, and Decoder for case classes, sealed traits, Scala 3 enums, Java enums, and more.

Installation

sbt

libraryDependencies += "com.kubuszok" %% "kindlings-circe-derivation" % "0.3.1"

Cross-platform (JVM / Scala.js / Scala Native):

libraryDependencies += "com.kubuszok" %%% "kindlings-circe-derivation" % "0.3.1"

Scala CLI

//> using dep com.kubuszok::kindlings-circe-derivation:0.3.1

Quick start

Encoding and decoding a case class
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-circe-derivation:0.3.1
//> using dep io.circe::circe-parser:0.14.15

import hearth.kindlings.circederivation._
import io.circe._

case class Person(name: String, age: Int)

// Inline encoding — no implicit needed
val json: Json = KindlingsEncoder.encode(Person("Alice", 30))
println(json.noSpaces)
// expected output:
// {"name":"Alice","age":30}

// Inline decoding
val parsed = io.circe.parser.parse("""{"name":"Bob","age":25}""")
println(parsed.flatMap(KindlingsDecoder.decode[Person](_)))
// expected output:
// Right(Person(Bob,25))

API

Derivation methods

Method Returns Description
KindlingsEncoder.derived[A] KindlingsEncoder[A] Sanely-automatic encoder (given/implicit, also usable as semi-automatic)
KindlingsEncoder.deriveAsObject[A] Encoder.AsObject[A] Object encoder
KindlingsEncoder.encode[A](value) Json Inline encoding (no instance allocation)
KindlingsDecoder.derived[A] KindlingsDecoder[A] Sanely-automatic decoder (given/implicit, also usable as semi-automatic)
KindlingsDecoder.decode[A](json) Either[DecodingFailure, A] Inline decoding
KindlingsCodecAsObject.derived[A] KindlingsCodecAsObject[A] Sanely-automatic codec (given/implicit, also usable as semi-automatic)

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

Type hierarchy

KindlingsEncoder[A] extends Encoder[A] and KindlingsDecoder[A] extends Decoder[A], so derived instances work anywhere the original Circe types are expected.

Configuration

All derivation methods accept an implicit Configuration:

import hearth.kindlings.circederivation._

implicit val config: Configuration = Configuration.default
  .withSnakeCaseMemberNames
  .withDiscriminator("type")
  .withDefaults
Builder method Description
withSnakeCaseMemberNames fieldNamefield_name
withKebabCaseMemberNames fieldNamefield-name
withPascalCaseMemberNames fieldNameFieldName
withScreamingSnakeCaseMemberNames fieldNameFIELD_NAME
withTransformMemberNames(f) Custom field name transform
withSnakeCaseConstructorNames MyTypemy_type in discriminator
withKebabCaseConstructorNames MyTypemy-type in discriminator
withPascalCaseConstructorNames MyTypeMyType in discriminator
withScreamingSnakeCaseConstructorNames MyTypeMY_TYPE in discriminator
withTransformConstructorNames(f) Custom constructor name transform
withDefaults Use case class default values for missing fields
withoutDefaults Require all fields (default)
withDiscriminator(field) ADT discriminator field name
withoutDiscriminator No discriminator (default — wrapping object)
withStrictDecoding Fail on unexpected JSON fields
withoutStrictDecoding Ignore unexpected fields (default)
withEnumAsStrings Encode Scala 3 / Java enums as strings

Annotations

Annotation Description
@fieldName("json_name") Override JSON field name for a case class field
@transientField Exclude a field from encoding/decoding (must have a default value)
import hearth.kindlings.circederivation.annotations._

case class User(
  @fieldName("user_name") name: String,
  @transientField internalId: Long = 0L
)

Usage examples

Sealed trait with discriminator
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-circe-derivation:0.3.1
//> using dep io.circe::circe-parser:0.14.15

import hearth.kindlings.circederivation._
import io.circe._

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

implicit val config: Configuration = Configuration.default
  .withDiscriminator("type")
  .withSnakeCaseConstructorNames

val shape: Shape = Circle(5.0)
println(KindlingsEncoder.encode(shape).noSpaces)
// expected output:
// {"type":"circle","radius":5.0}

val decoded = io.circe.parser.parse("""{"width":3,"height":4,"type":"rectangle"}""")
  .flatMap(KindlingsDecoder.decode[Shape](_))
println(decoded)
// expected output:
// Right(Rectangle(3.0,4.0))
Recursive data types
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-circe-derivation:0.3.1

import hearth.kindlings.circederivation._
import io.circe._

case class Tree(value: String, children: List[Tree])

val tree = Tree("root", List(
  Tree("left", Nil),
  Tree("right", List(Tree("leaf", Nil)))
))
println(KindlingsEncoder.encode(tree).noSpaces)
// expected output:
// {"value":"root","children":[{"value":"left","children":[]},{"value":"right","children":[{"value":"leaf","children":[]}]}]}
Case class with defaults
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-circe-derivation:0.3.1
//> using dep io.circe::circe-parser:0.14.15

import hearth.kindlings.circederivation._
import io.circe._

implicit val config: Configuration = Configuration.default.withDefaults

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

val parsed = io.circe.parser.parse("""{"host":"localhost"}""")
println(parsed.flatMap(KindlingsDecoder.decode[Settings](_)))
// expected output:
// Right(Settings(localhost,8080,false))

Debugging

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

import hearth.kindlings.circederivation.debug._

Or enable project-wide via scalac option:

// build.sbt
scalacOptions += "-Xmacro-settings:circeDerivation.logDerivation=true"

Comparison with circe-generic

Feature differences

Feature circe-generic Kindlings
Same API on Scala 2.13 and 3 No (different modules, different APIs) Yes
Automatic derivation without overhead No (re-derives at every use site) Yes (sanely-automatic)
Inline encoding/decoding No Yes (encode[A], decode[A])
Recursive types Needs Lazy / workarounds Just works
Named tuples No Yes
Opaque types No Yes
Scala 3 enums Partial (parameterless cases only, via ConfiguredEnum*) Yes (full enums)
Java enums No Yes
@ConfiguredJsonCodec annotation Yes No (use Configuration directly)

Benchmarks

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

Encode

Type Scala Kindlings semi Kindlings auto Original semi Original auto vs best original
SimpleCC 2.13 30.3M 30.9M 18.8M 19.0M 1.63x faster
SimpleCC 3 31.2M 31.2M 21.8M 20.9M 1.43x faster
SimpleADT 2.13 27.5M 27.1M 13.4M 13.9M 1.98x faster
SimpleADT 3 26.8M 25.7M 26.6M 27.1M 0.99x faster
Person 2.13 4.5M 4.5M 3.0M 3.1M 1.45x faster
Person 3 4.4M 4.5M 3.1M 3.2M 1.41x faster
Event 2.13 3.4M 3.4M 2.3M 2.4M 1.42x faster
Event 3 3.3M 3.4M 2.4M 2.3M 1.42x faster

Decode

Type Scala Kindlings semi Kindlings auto Original semi Original auto vs best original
SimpleCC 2.13 88.3M 93.2M 42.0M 42.6M 2.19x faster
SimpleCC 3 91.9M 92.1M 20.5M 21.2M 4.34x faster
SimpleADT 2.13 56.3M 55.9M 25.0M 25.7M 2.19x faster
SimpleADT 3 58.3M 54.6M 27.9M 28.0M 2.08x faster
Person 2.13 5.4M 5.3M 3.5M 3.6M 1.50x faster
Person 3 5.5M 5.4M 2.7M 2.6M 2.04x faster
Event 2.13 3.3M 3.5M 2.7M 2.7M 1.30x faster
Event 3 3.5M 3.3M 2.1M 2.2M 1.59x faster

End-to-end with jsoniter-scala-circe booster

jsoniter-scala-circe is a Circe booster that replaces the default parser/printer with jsoniter-scala's faster implementation. The table below benchmarks the full pipeline (domain type to bytes/String).

The booster is an optional add-on — Kindlings works with standard Circe parsing out of the box. When combined with the booster, Kindlings + jsoniter-scala-circe is the fastest way to use Circe.

Encode (domain type to bytes/String)

Type Scala Kindlings + booster Original + booster Kindlings (no booster) Original (no booster)
SimpleCC 2.13 13.9M 10.5M 6.8M 5.4M
SimpleCC 3 15.5M 12.0M 7.2M 6.7M
SimpleADT 2.13 14.3M 8.1M 7.8M 5.9M
SimpleADT 3 15.6M 11.7M 8.1M 6.9M
Person 2.13 1.6M 1.4M 985K 882K
Person 3 1.7M 1.5M 1.1M 964K
Event 2.13 1.3M 1.1M 831K 764K
Event 3 1.4M 1.2M 939K 805K

Decode (bytes/String to domain type)

Type Scala Kindlings + booster Original + booster Kindlings (no booster) Original (no booster)
SimpleCC 2.13 9.3M 8.1M 6.1M 5.9M
SimpleCC 3 8.8M 6.6M 7.1M 5.9M
SimpleADT 2.13 11.2M 9.1M 8.7M 7.4M
SimpleADT 3 10.9M 9.2M 9.8M 8.5M
Person 2.13 1.3M 1.1M 918K 879K
Person 3 1.3M 1.0M 1.1M 874K
Event 2.13 1.0M 906K 736K 724K
Event 3 996K 825K 836K 703K

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