Generic Module

The ciris-generic module provides the ability to decode products and coproducts using shapeless. This allows you to decode case classes, value classes, and shapeless coproducts, plus anything else that shapeless Generic supports. Note that care has been taken to make sure the generic module does not override default behaviours defined in the core module. The generic module might still override other defined behaviours, so use with care.

Let’s take a look at some examples. We start by defining a source from which we can read configuration values.

import ciris.{ConfigKeyType, ConfigSource}
import ciris.generic._

val source = {
  val keyType = ConfigKeyType[String]("generic key")
  ConfigSource.fromEntries(keyType)("key" -> "5.0")

We can then define and load a unary product, for example a case class with one value.

final case class DoubleValue(value: Double)
// defined class DoubleValue"key").decodeValue[DoubleValue]
// res0: ciris.ConfigEntry[ciris.api.Id,String,String,DoubleValue] = ConfigEntry(key, ConfigKeyType(generic key))

It also works for value classes and any other unary products shapeless Generic supports.

final case class FloatValue(val value: Float) extends AnyVal
// defined class FloatValue"key").decodeValue[FloatValue]
// res1: ciris.ConfigEntry[ciris.api.Id,String,String,FloatValue] = ConfigEntry(key, ConfigKeyType(generic key))

If we define a product with more than one value:

final case class TwoValues(value1: Double, value2: Float)
// defined class TwoValues

we will try to decode it twice, once as a Double, and once as a Float."key").decodeValue[TwoValues]
// res2: ciris.ConfigEntry[ciris.api.Id,String,String,TwoValues] = ConfigEntry(key, ConfigKeyType(generic key))

You can also customize the decoding on a per-type basis. For example, if you have a ConfigSource which reads values of type Map[String, String], you can customize which key gets decoded for which type, as in the example below.

import ciris.ConfigDecoder
// import ciris.ConfigDecoder

val mapSource = {
  val keyType = ConfigKeyType[String]("generic key")
    "key" -> Map(
      "key1" -> "1.0",
      "key2" -> "2.0"
// mapSource: ciris.ConfigSource[ciris.api.Id,String,scala.collection.immutable.Map[String,String]] = ConfigSource(ConfigKeyType(generic key))

implicit val decodeDouble: ConfigDecoder[Map[String, String], Double] =
  ConfigDecoder.catchNonFatal("Double")(map => map("key1").toDouble)
// decodeDouble: ciris.ConfigDecoder[Map[String,String],Double] = ConfigDecoder$444945982

implicit val decodeFloat: ConfigDecoder[Map[String, String], Float] =
  ConfigDecoder.catchNonFatal("Float")(map => map("key2").toFloat)
// decodeFloat: ciris.ConfigDecoder[Map[String,String],Float] = ConfigDecoder$413624547"key").decodeValue[TwoValues]
// res3: ciris.ConfigEntry[ciris.api.Id,String,scala.collection.immutable.Map[String,String],TwoValues] = ConfigEntry(key, ConfigKeyType(generic key))

We can also define a shapeless coproduct and attempt to decode it.

import shapeless.{:+:, CNil}
// import shapeless.{$colon$plus$colon, CNil}

type DoubleOrFloat = DoubleValue :+: FloatValue :+: CNil
// defined type alias DoubleOrFloat"key").decodeValue[DoubleOrFloat]
// res4: ciris.ConfigEntry[ciris.api.Id,String,String,DoubleOrFloat] = ConfigEntry(key, ConfigKeyType(generic key))

If there is no public constructor or apply method:

object PrivateValues {
  final class PrivateFloatValue private(val value: Float) extends AnyVal
  object PrivateFloatValue {
    def withValue(value: Float) =
      new PrivateFloatValue(value)
// defined object PrivateValues

import PrivateValues._
// import PrivateValues._

we will not be able to decode values, resulting in an error at compile-time."key").decodeValue[PrivateFloatValue]
// <console>:26: error: could not find implicit value for parameter decoder: ciris.ConfigDecoder[String,PrivateValues.PrivateFloatValue]
//                                      ^