Skip to content

Jsoniter Scala Derivation

Drop-in replacement for jsoniter-scala-macros JsonCodecMaker — derives JsonValueCodec, JsonCodec, and JsonKeyCodec for case classes, sealed traits, Scala 3 enums, Java enums, and more.

Installation

sbt

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

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

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

Scala CLI

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

Note

You also need jsoniter-scala-core as a runtime dependency:

libraryDependencies += "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.38.12"

Quick start

Encoding and decoding with JsonValueCodec
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-jsoniter-derivation:0.2.0
//> using dep com.kubuszok::kindlings-fast-show-pretty:0.2.0
//> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:2.38.12

import hearth.kindlings.jsoniterderivation._
import hearth.kindlings.fastshowpretty._
import com.github.plokhotnyuk.jsoniter_scala.core._

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

implicit val codec: JsonValueCodec[Person] = KindlingsJsonValueCodec.derived[Person]

val bytes = writeToArray(Person("Alice", 30))
println(new String(bytes))
// expected output:
// {"name":"Alice","age":30}

val person = readFromArray[Person]("""{"name":"Bob","age":25}""".getBytes)
println(FastShowPretty.render(person, RenderConfig.Default))
// expected output:
// Person(
//   name = "Bob",
//   age = 25
// )

API

Derivation methods

Method Returns Description
KindlingsJsonValueCodec.derived[A] KindlingsJsonValueCodec[A] Sanely-automatic codec (given/implicit, also usable as semi-automatic)
KindlingsJsonValueCodec.writeToString[A](value) String Inline encoding
KindlingsJsonValueCodec.readFromString[A](json) Either[JsonReaderException, A] Inline decoding
KindlingsJsonCodec.derived[A] KindlingsJsonCodec[A] Sanely-automatic combined value + key codec (given/implicit, also usable as semi-automatic)
KindlingsJsonCodec.deriveKeyCodec[A] JsonKeyCodec[A] Key codec (for map keys)

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

Configuration

import hearth.kindlings.jsoniterderivation._

implicit val config: JsoniterConfig = JsoniterConfig.default
  .withSnakeCaseFieldNames
  .withDiscriminator("type")
  .withSkipUnexpectedFields(true)
Builder method Description
withSnakeCaseFieldNames fieldNamefield_name
withKebabCaseFieldNames fieldNamefield-name
withPascalCaseFieldNames fieldNameFieldName
withScreamingSnakeCaseFieldNames fieldNameFIELD_NAME
withFieldNameMapper(f) Custom field name transform
withSnakeCaseAdtLeafClassNames ADT subtype name → snake_case
withKebabCaseAdtLeafClassNames ADT subtype name → kebab-case
withAdtLeafClassNameMapper(f) Custom ADT subtype name transform
withDiscriminator(field) ADT discriminator field name
withSkipUnexpectedFields(skip) Skip unexpected JSON fields
withEnumAsStrings Encode enums as strings
withMapAsArray Encode maps as arrays of key-value pairs
withStringified Encode numbers/booleans as strings
withDecodingOnly Derive only decoder
withEncodingOnly Derive only encoder
withCirceLikeObjectEncoding Encode ADTs as {"SubType": {...}}
withTransientDefault Skip fields with default values during encoding
withTransientEmpty Skip empty collections during encoding
withTransientNone Skip None fields during encoding
withRequireCollectionFields Fail if collection field is missing (instead of empty default)
withRequireDefaultFields Fail if field with default is missing
withCheckFieldDuplication Fail on duplicate JSON field names
withBigDecimalPrecision(n) Max BigDecimal precision
withBigDecimalScaleLimit(n) Max BigDecimal scale
withBigDecimalDigitsLimit(n) Max BigDecimal digits
withMapMaxInsertNumber(n) Max map entries
withSetMaxInsertNumber(n) Max set entries
withUseScalaEnumValueId Use enum value id for Scala enumerations

Annotations

Annotation Description
@fieldName("json_name") Override JSON field name
@transientField Exclude field from codec (must have default)
@stringified Encode this field as a string

Usage examples

Sealed trait with discriminator
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-jsoniter-derivation:0.2.0
//> using dep com.kubuszok::kindlings-fast-show-pretty:0.2.0
//> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:2.38.12

import hearth.kindlings.jsoniterderivation._
import hearth.kindlings.fastshowpretty._
import com.github.plokhotnyuk.jsoniter_scala.core._

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

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

implicit val codec: JsonValueCodec[Shape] = KindlingsJsonValueCodec.derived[Shape]

println(writeToString[Shape](Circle(5.0)))
// expected output:
// {"type":"Circle","radius":5.0}

val decoded: Shape = readFromString[Shape]("""{"type":"Rectangle","width":3,"height":4}""")
println(FastShowPretty.render(decoded, RenderConfig.Default))
// expected output:
// (Rectangle(
//     width = 3.0d,
//     height = 4.0d
//   )): Shape
Transient fields and defaults
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-jsoniter-derivation:0.2.0
//> using dep com.kubuszok::kindlings-fast-show-pretty:0.2.0
//> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:2.38.12

import hearth.kindlings.jsoniterderivation._
import hearth.kindlings.fastshowpretty._
import com.github.plokhotnyuk.jsoniter_scala.core._

implicit val config: JsoniterConfig = JsoniterConfig.default
  .withTransientDefault
  .withTransientNone

case class Settings(
  host: String,
  port: Int = 8080,
  debug: Option[Boolean] = None
)

implicit val codec: JsonValueCodec[Settings] = KindlingsJsonValueCodec.derived[Settings]

// Default and None fields are omitted
println(writeToString(Settings("localhost")))
// expected output:
// {"host":"localhost"}

// Missing fields use defaults
println(FastShowPretty.render(readFromString[Settings]("""{"host":"example.com"}"""), RenderConfig.Default))
// expected output:
// Settings(
//   host = "example.com",
//   port = 8080,
//   debug = None
// )

Debugging

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

import hearth.kindlings.jsoniterderivation.debug._

Or enable project-wide via scalac option:

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

Comparison with jsoniter-scala macros

Feature differences

Feature jsoniter-scala macros Kindlings
Same API on Scala 2.13 and 3 Partial (different compile deps) Yes
Sanely-automatic derivation No Yes
Inline encoding/decoding No Yes (writeToString, readFromString)
Recursive types Needs workarounds Just works
Named tuples No Yes
Opaque types No Yes
Scala 3 enums Yes Yes
Java enums Yes Yes
make[A] convenience factory Yes No (use derive[A])

Benchmarks

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

Note

Kindlings is at parity with jsoniter-scala's own macros: writes and reads are ~tied (within ~1.1x in both directions). The only consistent gap is SimpleCC read on Scala 2.13 (0.92x).

Write

Type Scala Kindlings semi Kindlings auto Original semi vs original
SimpleCC 2.13 59.5M 60.2M 62.4M 0.97x
SimpleCC 3 62.0M 62.4M 58.4M 1.07x faster
Person 2.13 4.5M 4.5M 4.4M ~tied
Person 3 5.3M 5.3M 5.2M ~tied
Event 2.13 4.0M 3.7M 4.1M ~tied
Event 3 4.7M 4.7M 4.7M ~tied

Read

Type Scala Kindlings semi Kindlings auto Original semi vs original
SimpleCC 2.13 29.7M 31.2M 33.7M 0.92x
SimpleCC 3 34.6M 34.5M 33.9M ~tied
Person 2.13 2.6M 3.5M 3.1M 1.11x faster
Person 3 3.6M 3.6M 3.6M ~tied