scalaconfigeitherhoconpureconfig

Representing Either in pureconfig


I have a HOCON config like this:

[
    {
        name = 1
        url = "http://example.com"
    },
    {
        name = 2
        url = "http://example2.com"
    },
    {
        name = 3
        url = {
            A = "http://example3.com"
            B = "http://example4.com"
        }
    }
]

I want to parse it with pureconfig. How can I represent that the URL can be either a string or a map of multiple urls, each having a key?

I have tried this:

import pureconfig.ConfigSource
import pureconfig.generic.auto.exportReader

case class Site(name: Int, url: Either[String, Map[String, String]])
case class Config(sites: List[Site])
ConfigSource.default.loadOrThrow[Config]

But it resulted in "Expected type OBJECT. Found STRING instead."

I know pureconfig supports Option. I have found no mention of supporting Either, does it mean it can be replaced with something else?


Solution

  • As you can see Either in not on list of types supported out of the box.

    However Either falls under sealed family, so:

    @ ConfigSource.string("""{ type: left, value: "test" }""").load[Either[String, String]]
    res15: ConfigReader.Result[Either[String, String]] = Right(Left("test"))
    
    @ ConfigSource.string("""{ type: right, value: "test" }""").load[Either[String, String]]
    res16: ConfigReader.Result[Either[String, String]] = Right(Right("test"))
    

    works. If you have a sealed hierarchy, what pureconfig will do is require an object which has a field type - this field will be used to dispatch parsing to a specific subtype. All the other fields will be passed as fields to parse into that subtype.

    If that doesn't work for you, you might try to implement the codec yourself:

    // just an example
    implicit def eitherReader[A: ConfigReader, B: ConfigReader] =
      new ConfigReader[Either[A, B]] {
        def from(cur: ConfigCursor) =
          // try left, if fail try right
          ConfigReader[A].from(cur).map(Left(_)) orElse ConfigReader[B].from(cur).map(Right(_))
      }
    

    which now will not require discrimination value:

    @ ConfigSource.string("""{ test: "test" }""").load[Map[String, Either[String, String]]]
    res26: ConfigReader.Result[Map[String, Either[String, String]]] = Right(Map("test" -> Left("test")))
    

    This is not provided by default because you would have to answer a few things yourself:

    If you have an idea what is expected behavior you can implement your own codec and use it in derivation.