ZIO/ZLayer FAQ: How to use a Java Properties files with ZIO

ZIO/ZLayer FAQ: How do I use a Java Properties file with ZIO 2 and Scala?

Solution

You can use the zio-config library for things like this, but at the moment my preferred approach is to hand-code this ZLayer solution. Maybe that’s because I know how to work with a Java Properties file — i.e., how to read and load it — so I like to see those details.

Therefore, given this Java properties file named application.properties:

username=alvin
email=coyote@acme.org

The following ZIO 2 and Scala 3 code shows how to read that properties file when it’s located in the same directory in which you start this application. I have added a lot of comments to the code, so please see the comments and Scala code for more information:

import zio.*
import java.io.FileInputStream
import java.util.Properties

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

// read the properties file as usual, wrapping that with ZIO.attempt
def readProperties(filePath: String) = ZIO.attempt {
    val properties = new Properties()
    properties.load(new FileInputStream(filePath))
    properties
}

// access the individual properties to create an AppConfig instance
def propertiesToConfig(properties: Properties): Task[AppConfig] = ZIO.attempt {
    val username = properties.getProperty("username")
    val email = properties.getProperty("email")
    AppConfig(username, email)
}

/**
 * Here I create a ZLayer named `live` by actually doing the work of
 * reading the properties file, populating an AppConfig instance, and 
 * then returning that instance.
 */
object PropertiesConfig:
    def live(filePath: String): ZLayer[Any, Throwable, AppConfig] = ZLayer {
        for
            properties <- readProperties(filePath)
            config     <- propertiesToConfig(properties)
        yield
            config
    }

/**
 * 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
 * Can also use zio-config for app configuration.
 */
object ZioZLayerJavaPropertiesFile extends ZIOAppDefault:

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

    val run =
        app.provideLayer(PropertiesConfig.live("./application.properties"))
           .exitCode

An interesting/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 and location of the configuration file, and I pass that parameter into the live function from inside the run value. At the moment you can’t find solutions like this on the internet, so I hope this is helpful.

Because I added many comments to that code, again, please see those comments for more details.