Pureconfig read config as properties map

2.2k views Asked by At

Is it possible to make pureconfig read properties as Map[String, String]? I have the following

application.conf:

cfg{
  some.property.name: "value"
  some.another.property.name: "another value"
}

Here is the application I tried to read the config with:

import pureconfig.generic.auto._
import pureconfig.ConfigSource
import pureconfig.error.ConfigReaderException

object Model extends App {
  case class Config(cfg: Map[String, String])

  val result = ConfigSource.default
    .load[Config]
    .left
    .map(err => new ConfigReaderException[Config](err))
    .toTry

  val config = result.get
  println(config)
}

The problem is it throws the following excpetion:

Exception in thread "main" pureconfig.error.ConfigReaderException: Cannot convert configuration to a Model$Config. Failures are:
  at 'cfg.some':
    - (application.conf @ file:/home/somename/prcfg/target/classes/application.conf: 2-3) Expected type STRING. Found OBJECT instead.

    at Model$.$anonfun$result$2(Model.scala:11)
    at scala.util.Either$LeftProjection.map(Either.scala:614)
    at Model$.delayedEndpoint$Model$1(Model.scala:11)
    at Model$delayedInit$body.apply(Model.scala:5)
    at scala.Function0.apply$mcV$sp(Function0.scala:39)
    at scala.Function0.apply$mcV$sp$(Function0.scala:39)
    at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:17)
    at scala.App.$anonfun$main$1(App.scala:73)
    at scala.App.$anonfun$main$1$adapted(App.scala:73)
    at scala.collection.IterableOnceOps.foreach(IterableOnce.scala:553)
    at scala.collection.IterableOnceOps.foreach$(IterableOnce.scala:551)
    at scala.collection.AbstractIterable.foreach(Iterable.scala:920)
    at scala.App.main(App.scala:73)
    at scala.App.main$(App.scala:71)
    at Model$.main(Model.scala:5)
    at Model.main(Model.scala)

Is there a way to fix it? I expected that the Map[String, String] will contain the following mappings:

some.property.name -> "value"
some.another.property.name -> "another value"
2

There are 2 answers

1
Mateusz Kubuszok On BEST ANSWER

Your issue is not pureconfig. Your issue is that by HOCON spec what you wrote:

cfg {
  some.property.name: "value"
  some.another.property.name: "another value"
}

is a syntactic sugar for:

cfg {
  some {
    property {
      name = "value"
    }
  }
  
  another {
    property {
      name = "another value"
    }
  }
}

It's TypeSafe Config/Lightbend Config who decides that your cfg has two properties and both of them are nested configs. Pureconfig only takes these nested configs and maps them into case classes. But it won't be able to map something which has a radically different structure then expected.

If you write:

cfg {
  some-property-name: "value"
  some-another-property-name: "another value"
}

You'll be able to decode "cfg" path as Map[String, String] and top level config as case class Config(cfg: Map[String, String]). If you wanted to treat . as part of the key and not nesting... then I'm afraid you have to write a ConfigReader yourself because that is non-standard usage.

0
Matthias Berndt On

You can read a Map[String, String] in that way with the following ConfigReader:

implicit val strMapReader: ConfigReader[Map[String, String]] = {
  implicit val r: ConfigReader[String => Map[String, String]] =
    ConfigReader[String]
      .map(v => (prefix: String) => Map(prefix -> v))
      .orElse { strMapReader.map { v =>
        (prefix: String) => v.map { case (k, v2) => s"$prefix.$k" -> v2 }
      }}
  ConfigReader[Map[String, String => Map[String, String]]].map {
    _.flatMap { case (prefix, v) => v(prefix) }
  }
}

Note that this is a recursive val definition, because strMapReader is used within its own definition. The reason it works is that the orElse method takes its parameter by name and not by value.