Custom Readers

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 readers 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 ConfigReader in scope.

env[Odd]("ODD_VALUE")
// <console>:18: error: could not find implicit value for evidence parameter of type ciris.ConfigReader[Odd]
//        env[Odd]("ODD_VALUE")
//                ^

This means we’ll have to define a custom implicit ConfigReader[Odd] instance. The ConfigReader companion object provides some helper methods for creating ConfigReaders. In this case, we’ll rely on the default ConfigReader[Int] instance to read an Int and we will then try to convert the Int to an Odd instance using the mapOption method. Most ConfigReader methods accept a typeName argument which is the name of the type you’re reading. Depending on which Scala version and which platform (Scala, Scala.js, …) 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 oddConfigReader(implicit intReader: ConfigReader[Int]) =
  intReader.mapOption("Odd")(odd)
// oddConfigReader: (implicit intReader: ciris.ConfigReader[Int])ciris.ConfigReader[Odd]

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

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

val a = read[Odd]("a")
// a: ciris.ConfigValue[Odd] = ConfigValue(Left(WrongType(a, abc, Int, ConfigKeyType(int key), Some(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 = read[Odd]("b")
// b: ciris.ConfigValue[Odd] = ConfigValue(Left(WrongType(b, 6, Odd, ConfigKeyType(int key), None)))

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

read[Odd]("c")
// res3: ciris.ConfigValue[Odd] = ConfigValue(Right(Odd(3)))

While this demonstrates how to create custom ConfigReaders, 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.