Config Writers

The main use case for PureConfig, as described in the homepage, is to load configuration files to Scala classes in a typesafe and boilerplate-free way. However, there are situations where users may have the need to do the inverse operation: to write a config file from a Scala data structure. An example would be to save a config after it is changed in-app.

Just as PureConfig provides a ConfigReader interface for reading configurations, it also provides a ConfigWriter for writing configs.

All types mentioned at Built-in Supported Types are supported both in reading and in writing operations:

import pureconfig._
import pureconfig.generic.auto._

sealed trait MyAdt
case class AdtA(a: String) extends MyAdt
case class AdtB(b: Int) extends MyAdt
final case class Port(value: Int) extends AnyVal
case class MyClass(
  boolean: Boolean,
  port: Port,
  adt: MyAdt,
  list: List[Double],
  map: Map[String, String],
  option: Option[String])
  
val confObj = MyClass(true, Port(8080), AdtB(1), List(1.0, 0.2), Map("key" -> "value"), None)
ConfigWriter[MyClass].to(confObj)
// res1: com.typesafe.config.ConfigValue = SimpleConfigObject({"adt":{"b":1,"type":"adt-b"},"boolean":true,"list":[1.0,0.2],"map":{"key":"value"},"port":8080})

The mechanisms with which PureConfig finds out how to write a type to a config are the same as ones used with ConfigReader. Therefore, you can use most tutorials and tips at Supporting New Types and Overriding Behavior for Types for creating ConfigWriter instances, too. ConfigWriter also has useful combinators and factory methods to simplify new implementations:

class MyInt(value: Int) {
  def getValue: Int = value
  override def toString: String = s"MyInt($value)"
}

implicit val myIntWriter = ConfigWriter[Int].contramap[MyInt](_.getValue)
ConfigWriter[MyInt].to(new MyInt(1))
// res2: com.typesafe.config.ConfigValue = ConfigInt(1)

Finally, if you need both the reading and the writing part for a custom type, you can implement a ConfigConvert:

implicit val myIntConvert = ConfigConvert[Int].xmap[MyInt](new MyInt(_), _.getValue)
val conf = ConfigWriter[MyInt].to(new MyInt(1))
// conf: com.typesafe.config.ConfigValue = ConfigInt(1)
ConfigReader[MyInt].from(conf)
// res3: ConfigReader.Result[MyInt] = Right(MyInt(1))

A ConfigConvert implements both the ConfigReader and ConfigWriter interfaces and can be used everywhere one of them is needed.