Custom Decoders

Ciris already supports reading many standard library types and provides integrations with libraries like enumeratum, refined, and squants. If you’re trying to load a standard library type, it is likely that it should be supported by Ciris, so please file an issue or, even better, submit a pull-request. The same applies if you’re trying to integrate with a library for which Ciris does not already provide a module.

However, there may be cases where you need to resort to defining custom decoders for your types. For example, let’s say you’re dealing with a sealed Odd class, where you can only construct instances from an odd method, which accepts an Int and returns an Option[Odd].

import ciris._
// import ciris._

sealed abstract case class Odd(value: Int)
// defined class Odd

def odd(value: Int): Option[Odd] = {
  Option(value)
    .filter(_ % 2 == 1)
    .map(new Odd(_) {})
}
// odd: (value: Int)Option[Odd]

We would now like to load Odd values using Ciris. If we try and do this straight away, it will fail during compile, saying there is no implicit ConfigDecoder in scope.

env[Odd]("ODD_VALUE")
// <console>:18: error: could not find implicit value for parameter decoder: ciris.ConfigDecoder[String,Odd]
//        env[Odd]("ODD_VALUE")
//                ^

This means we’ll have to define a custom implicit ConfigDecoder[String, Odd] instance. The ConfigDecoder companion object provides some helper methods for creating ConfigDecoders. In this case, we’ll go one step further and define a ConfigDecoder[A, Odd] by relying on an existing ConfigDecoder[A, Int] to first decode to an Int. We’ll then use mapOption to convert the Int to an Odd. Most ConfigDecoder methods accept a typeName argument which is the name of the type you’re decoding. Depending on which Scala version and which platform (Scala, Scala.js, Scala Native) you run on, you may be able to use type tags. In this case, and cases where you don’t have type parameters, it’s simple enough to just provide the type name.

implicit def oddConfigDecoder[A](
  implicit decoder: ConfigDecoder[A, Int]
): ConfigDecoder[A, Odd] = {
  decoder.mapOption("Odd")(odd)
}
// oddConfigDecoder: [A](implicit decoder: ciris.ConfigDecoder[A,Int])ciris.ConfigDecoder[A,Odd]

We can then try to read Odd values from a custom configuration source.

val source = {
  val keyType = ConfigKeyType[String]("int key")
  ConfigSource.fromMap(keyType)(Map("a" -> "abc", "b" -> "6", "c" -> "3"))
}
// source: ciris.ConfigSource[ciris.api.Id,String,String] = ConfigSource(ConfigKeyType(int key))

val a = source.read("a").decodeValue[Odd]
// a: ciris.ConfigEntry[ciris.api.Id,String,String,Odd] = ConfigEntry(a, ConfigKeyType(int key), Right(abc), Left(WrongType(a, ConfigKeyType(int key), Right(abc), abc, Int, java.lang.NumberFormatException: For input string: "abc")))

a.value.left.map(_.message)
// res1: scala.util.Either[String,Odd] = Left(Int key [a] with value [abc] cannot be converted to type [Int]: java.lang.NumberFormatException: For input string: "abc")

val b = source.read("b").decodeValue[Odd]
// b: ciris.ConfigEntry[ciris.api.Id,String,String,Odd] = ConfigEntry(b, ConfigKeyType(int key), Right(6), Left(WrongType(b, ConfigKeyType(int key), Right(6), 6, Odd)))

b.value.left.map(_.message)
// res2: scala.util.Either[String,Odd] = Left(Int key [b] with value [6] cannot be converted to type [Odd])

source.read("c").decodeValue[Odd]
// res3: ciris.ConfigEntry[ciris.api.Id,String,String,Odd] = ConfigEntry(c, ConfigKeyType(int key), Right(3), Right(Odd(3)))

While this demonstrates how to create custom ConfigDecoders, a better way to represent Odd values is by using the Odd predicate from refined. The Encoding Validation section has more information on the ciris-refined module and how you can use it to read refined types.