How do I get an unwrapped key in Typesafe Config?

Chad Retz picture Chad Retz · Feb 12, 2014 · Viewed 11.9k times · Source

Test case:

import org.specs2.mutable._
class HelloWorldSpec extends Specification {
  "Typesafe Config" should "allow me to see my escaped key" in {
    val entries = ConfigFactory.parseString("""
      "quoted.key.1" = 5
      "quoted.key.2" = 6""").entrySet
    entries.head.getKey === "quoted.key.1"
  }
}

This test fails because the key is actually "quoted.key.1", not quoted.key.1. Is there a suggested way to unwrap this or do I have to manually look for surrounding quotes and remove them every time?

Answer

Havoc P picture Havoc P · Feb 13, 2014

Read about "Paths, keys, and Config vs. ConfigObject" in the API docs here: http://typesafehub.github.io/config/latest/api/com/typesafe/config/Config.html and in the README here: https://github.com/typesafehub/config#understanding-config-and-configobject

(Suggestions for improving those docs are welcome.)

The keys in the entry set (and the Config) are path expressions. These are strings that require parsing. There is a parse method in ConfigUtil, see http://typesafehub.github.io/config/latest/api/com/typesafe/config/ConfigUtil.html#splitPath%28java.lang.String%29

It won't work to just remove quotes, the parsing is somewhat more complex than that. Fortunately you can just use the ConfigUtil.splitPath method.

So the two ways to iterate keys at the root level are something like, first with Config:

Config config = ... ;
for (Map.Entry<String, ConfigValue> entry: config.entrySet()) {
  String[] keys = ConfigUtil.splitPath(entry.getKey());
  System.out.println("Root key = " + keys[0]);
}

Then with ConfigObject:

Config config = ... ;
for (Map.Entry<String, ConfigValue> entry: config.root().entrySet()) {
  System.out.println("Root key = " + entry.getKey());
}

I didn't try compiling the above examples so forgive any silly syntax errors.

If your configuration contains only one level (no nested objects) the above two ways of iterating are the same; but if you have nested values they are not the same because iterating Config will give you all leaf values while iterating ConfigObject (config.root()) will give you all immediate children of the root, even if those immediate children are themselves objects.

Say you have:

foo {
   bar {
       baz = 10
   }
}

If you iterate that as a Config you will get one entry which will have the path foo.bar.baz as key and the value 10. If you iterate it as a ConfigObject then you will have one entry which will have the key foo and the value will be an object which will in turn contain the key bar. When iterating this as a Config you could splitPath the foo.bar.baz and you would get an array of three strings, foo, bar, and baz.

To convert a Config to a ConfigObject use the root() method and to convert ConfigObject to Config use the toConfig() method. So config.root().toConfig() == config.

Also, the above config file could be equivalently written as:

foo.bar.baz = 10

But would be different if written as:

"foo.bar.baz" = 10

Because in the first case you have nested objects, and in the second case you have a single object with periods in the key name. This is due to the quotes.

If you write "foo.bar.baz" with quotes, then when iterating Config the path you got back would be quoted and splitPath() would return an array of one element foo.bar.baz. When iterating ConfigObject you would have a single object which would contain an entry with foo.bar.baz as key and 10 as value. Keys containing ., or other special characters, have to be quoted so they are interpreted as single keys rather than as paths.

To make your test case pass you could do this using splitPath:

import org.specs2.mutable._
class HelloWorldSpec extends Specification {
  "Typesafe Config" should "allow me to see my escaped key" in {
    val entries = ConfigFactory.parseString("""
      "quoted.key.1" = 5
      "quoted.key.2" = 6""").entrySet
    // the ordering of entrySet is not guaranteed so this may
    // still fail because it gets quoted.key.2 instead
    ConfigUtil.splitPath(entries.head.getKey).head === "quoted.key.1"
  }
}

You could also do this, using ConfigObject:

import org.specs2.mutable._
class HelloWorldSpec extends Specification {
  "Typesafe Config" should "allow me to see my escaped key" in {
    val entries = ConfigFactory.parseString("""
      "quoted.key.1" = 5
      "quoted.key.2" = 6""").root.entrySet // note ".root." here
    // the ordering of entrySet is not guaranteed so this may
    // still fail because it gets quoted.key.2 instead
    // no need to splitPath because ConfigObject has keys not paths
    entries.head.getKey === "quoted.key.1"
  }
}