ZIO/ZLayer FAQ: How to use a Typesafe Config HOCON properties file with ZIO

ZIO/ZLayer FAQ: How do I use a Typesafe Config HOCON properties file with ZIO?

Solution

For this ZIO ZLayer solution, you can use the zio-config library for things like this, but at the moment my preferred approach is to hand-code this solution. That’s probably because I’ve written code before to read a HOCON file, so it’s more straightforward atm.

Typesafe Config HOCON file location

Before we get into the solution, note that a Typesafe HOCON configuration file needs to be somewhere on your application classpath. For instance, if you’re running your application using SBT, the HOCON application.conf file needs to be in the src/main/resources directory (or somewhere similar). If the file isn’t in the right location, you’ll see error messages like this:

com.typesafe.config.ConfigException$Missing: system properties: No configuration setting found for key 'myapp'

Back to the solution

Getting back to the solution ... given this Typesafe HOCON properties file named application.properties:

myapp {
    username = "alvin"
    email = "coyote@acme.com"
}

The following ZIO 2 code shows how to read that HOCON file when it’s located where Typesafe wants it to be located. I’ve added a lot of comments to the code, so please see the comments and Scala code for more information:

package _zlayer_hocon

import zio.{Console, Task, ZIO, ZIOAppDefault, ZLayer}
import com.typesafe.config.{Config, ConfigFactory}

// these fields correspond to the HOCON properties file fields
case class AppProperties(username: String, email: String)

// read the HOCON file into a Config instance
def readHoconFile(filePath: String): Task[Config] = ZIO.attempt {
    val config: Config = ConfigFactory.load(filePath)
    config
}

// given a Config instance, create an AppProperties instance
// from those properties
def createAppPropertiesFromProperties(config: Config): Task[AppProperties] = ZIO.attempt {
    val username = config.getString("myapp.username")
    val email = config.getString("myapp.email")
    AppProperties(username, email)
}

// this object contains the ZLayer that opens and reads the HOCON
// configuration file, and populates an AppProperties instance
// based on that configuration information.
object HoconConfig:
    def live(filePath: String): ZLayer[Any, Throwable, AppProperties] = ZLayer {
        for
            properties    <- readHoconFile(filePath)
            appProperties <- createAppPropertiesFromProperties(properties)
        yield
            appProperties
    }

/**
 * This is the "main" ZIO 2 application, including an `app` value
 * and the required `run` value. The `run` value *provides* the
 * configuration information to the `app`.
 *
 * For ZLayer info, see:
 *     - https://zio.dev/reference/contextual/zlayer
 *
 * Note: Can also use zio-config for app configuration.
 */
object ZioZLayerHoconFile extends ZIOAppDefault:

    val app: ZIO[AppProperties, Throwable, Unit] = for
        appProps  <- ZIO.service[AppProperties]
        _         <- Console.printLine(s"USERNAME: ${appProps.username}")
        _         <- Console.printLine(s"EMAIL:    ${appProps.email}")
    yield
        ()

    val run =
        app
            // must be in 'src/main/resources'; does not work with absolute file location
            .provideLayer(HoconConfig.live("application.conf"))
            .foldZIO(
                failure => Console.printLineError(s"FAILURE = $failure"),
                success => Console.printLine(     s"SUCCESS = $success")
            )

When this ZIO 2 ZLayer application is run, you should see output like this:

USERNAME: alvin
EMAIL:    coyote@acme.com
SUCCESS = ()

Notes

One note I’ll add is that the readHoconFile and createAppPropertiesFromProperties functions can be combined into one function. I just show them separately to make the live function in HoconConfig a little more interesting.

Also, another interesting and unique part of this solution is that it shows how to pass a parameter into a ZLayer from inside the run value. By that, I mean that the ZLayer needs to know the name of the configuration file, and I pass that parameter into the live function inside the run value. At the moment you can’t find solutions like this on the internet, so I hope this is helpful.

As noted, I have a lot of comments in that code that describe how things work, so please see those comments for more information.