Skip to content

YAML Derivation

Drop-in replacement for scala-yaml's built-in derives — derives YamlEncoder, YamlDecoder, and YamlCodec for case classes, sealed traits, Scala 3 enums, Java enums, and more.

Installation

sbt

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

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

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

Scala CLI

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

Note

You also need scala-yaml as a runtime dependency:

libraryDependencies += "org.virtuslab" %%% "scala-yaml" % "0.3.1"

Quick start

Encoding and decoding a case class
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-yaml-derivation:0.2.0
//> using dep org.virtuslab::scala-yaml:0.3.1

import hearth.kindlings.yamlderivation._

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

// Encode to YAML string
val yaml: String = KindlingsYamlEncoder.toYamlString(Person("Alice", 30))
println(yaml)
// expected output:
// name: Alice
// age: 30

// Decode from YAML string
val result = KindlingsYamlDecoder.fromYamlString[Person]("name: Bob\nage: 25")
println(result)
// expected output:
// Right(Person(Bob,25))

API

Derivation methods

Method Returns Description
KindlingsYamlEncoder.derive[A] YamlEncoder[A] Semi-automatic encoder
KindlingsYamlEncoder.encode[A](value) Node Inline encoding (no instance allocation)
KindlingsYamlEncoder.toYamlString[A](value) String Inline encoding to YAML string
KindlingsYamlEncoder.derived[A] KindlingsYamlEncoder[A] Sanely-automatic (given/implicit)
KindlingsYamlDecoder.derive[A] YamlDecoder[A] Semi-automatic decoder
KindlingsYamlDecoder.decode[A](node) Either[ConstructError, A] Inline decoding from a YAML node
KindlingsYamlDecoder.fromYamlString[A](yaml) Either[YamlError, A] Inline decoding from a YAML string
KindlingsYamlDecoder.derived[A] KindlingsYamlDecoder[A] Sanely-automatic (given/implicit)
KindlingsYamlCodec.derive[A] KindlingsYamlCodec[A] Semi-automatic codec (encoder + decoder)
KindlingsYamlCodec.derived[A] KindlingsYamlCodec[A] Sanely-automatic codec

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

Type hierarchy

KindlingsYamlEncoder[A] extends YamlEncoder[A] and KindlingsYamlDecoder[A] extends YamlDecoder[A], so derived instances work anywhere the original scala-yaml types are expected.

Syntax extensions

The syntax object provides extension methods for convenient inline usage:

import hearth.kindlings.yamlderivation.syntax._

val yaml: String = Person("Alice", 30).toYamlString
val result: Either[YamlError, Person] = "name: Bob\nage: 25".fromYamlString[Person]

Configuration

All derivation methods accept an implicit YamlConfig:

import hearth.kindlings.yamlderivation._

implicit val config: YamlConfig = YamlConfig.default
  .withSnakeCaseMemberNames
  .withDiscriminator("type")
  .withUseDefaults
Builder method Description
withSnakeCaseMemberNames fieldName -> field_name
withKebabCaseMemberNames fieldName -> field-name
withPascalCaseMemberNames fieldName -> FieldName
withScreamingSnakeCaseMemberNames fieldName -> FIELD_NAME
withTransformMemberNames(f) Custom field name transform
withSnakeCaseConstructorNames MyType -> my_type in discriminator
withKebabCaseConstructorNames MyType -> my-type in discriminator
withTransformConstructorNames(f) Custom constructor name transform
withDiscriminator(field) ADT discriminator field name
withEnumAsStrings Encode Scala 3 / Java enums as strings
withUseDefaults Use case class default values for missing fields

Annotations

Annotation Description
@fieldName("yaml_name") Override YAML field name for a case class field
@transientField Exclude a field from encoding/decoding (must have a default value)
import hearth.kindlings.yamlderivation.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-yaml-derivation:0.2.0
//> using dep org.virtuslab::scala-yaml:0.3.1

import hearth.kindlings.yamlderivation._

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

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

val yaml = KindlingsYamlEncoder.toYamlString[Shape](Circle(5.0))
println(yaml)
// expected output:
// type: circle
// radius: 5.0

val decoded = KindlingsYamlDecoder.fromYamlString[Shape]("width: 3\nheight: 4\ntype: rectangle")
println(decoded)
// expected output:
// Right(Rectangle(3.0,4.0))
Case class with defaults
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-yaml-derivation:0.2.0
//> using dep org.virtuslab::scala-yaml:0.3.1

import hearth.kindlings.yamlderivation._

implicit val config: YamlConfig = YamlConfig.default.withUseDefaults

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

val result = KindlingsYamlDecoder.fromYamlString[Settings]("host: localhost")
println(result)
// expected output:
// Right(Settings(localhost,8080,false))
Snake-case field names
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-yaml-derivation:0.2.0
//> using dep org.virtuslab::scala-yaml:0.3.1

import hearth.kindlings.yamlderivation._

implicit val config: YamlConfig = YamlConfig.default
  .withSnakeCaseMemberNames

case class DatabaseConfig(hostName: String, portNumber: Int, maxConnections: Int)

val yaml = KindlingsYamlEncoder.toYamlString(DatabaseConfig("localhost", 5432, 10))
println(yaml)
// expected output:
// host_name: localhost
// port_number: 5432
// max_connections: 10

Debugging

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

import hearth.kindlings.yamlderivation.debug._

Or enable project-wide via scalac option:

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

Comparison with scala-yaml built-in derivation

Feature differences

Feature scala-yaml derives Kindlings
Same API on Scala 2.13 and 3 No (Scala 3 only) Yes
Sealed trait / ADT support No Yes
Configuration (naming, discriminator) No Yes (YamlConfig)
Field name annotations No Yes (@fieldName)
Default values No Yes (withUseDefaults)
Enum handling Broken in some cases Correct
Option decoding Broken in some cases Correct
Inline encoding/decoding No Yes (toYamlString, fromYamlString)
Sanely-automatic derivation No Yes