2

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"
Some Name
  • 8,555
  • 5
  • 27
  • 77

2 Answers2

5

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.

Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64
  • The background of the problems is to allow users to add config parameters in the properties-file format. – Some Name Oct 16 '20 at 23:01
2

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.

Matthias Berndt
  • 4,387
  • 1
  • 11
  • 25