ZIO Config

ZIO Config

  • Documentation
  • GitHub

›ZIO-CONFIG

ZIO-CONFIG

  • Quick Start
  • Manual creation of ConfigDescriptor
  • Automatic Derivation of ConfigDescriptor
  • Read from various Sources
  • Using ConfigDescriptor for Read, Write, Document and Report
  • Automatic Validations
  • Design Principles

Read from various Sources

zio-config supports various sources ranging from an in-memory Map, to environment variables, through to a HOCON file. Forming a source gets into a standard pattern, and is easy for you to add another one.

import zio.IO
import zio.config._, ConfigDescriptor._, ConfigSource._


case class MyConfig(ldap: String, port: Int, dburl: String)

To perform any action using zio-config, we need a configuration description. Let's define a simple one.


val myConfig =
  (string("LDAP") |@| int("PORT")|@| string("DB_URL"))(MyConfig.apply, MyConfig.unapply)

 // val automatedConfig = descriptor[MyConfig]; using zio-config-magnolia

More details about defining config descriptor is in here.

Constant Map


val mapSource =
  ConfigSource.fromMap(
    Map(
      "LDAP" -> "xyz",
      "PORT" -> "1222",
      "DB_URL" -> "postgres"
    )
  )

val io = read(myConfig from mapSource)
// Running io (which is a zio) to completion yields  MyConfig(xyz, 1222, postgres)

Alternatively you can follow below snippet, yielding Config[MyConfig], which you can use as ZIO environment.


ZConfig.fromMap(Map(), myConfig)
// yielding Config[MyConfig], which is a service of config that you can use as ZIO environments.


Multi Map Source

This support a list of values for a key.

case class ListConfig(ldap: String, port: List[Int], dburl: String)

val listConfig = (string("LDAP") |@| list("PORT")(int) |@| string("DB_URL"))(ListConfig.apply, ListConfig.unapply)

val multiMapSource =
  ConfigSource.fromMultiMap(
    Map(
      "LDAP" -> ::("xyz",  Nil),
      "PORT" -> ::("1222" , "2221" :: Nil),
      "DB_URL" -> ::("postgres",  Nil)
    )
  )

read(myConfig from multiMapSource)
// Running this to completion yields ListConfig(xyz, List(1222, 2221), postgres)

Alternatively you can follow below snippet, yielding Config[MyConfig], which you can use as ZIO environment.


ZConfig.fromMultiMap(Map(), myConfig, "constant")
// yielding Config[MyConfig], which is a service of config that you can use as ZIO environments.

System Environment


val sysEnvSource =
  ConfigSource.fromSystemEnv

// If you want to support list of values, then you should be giving a valueDelimiter
val sysEnvSourceSupportingList =
  ConfigSource.fromSystemEnv(keyDelimiter = None, valueDelimiter = Some(','))

// If you want to consider system-env as a nested config, provide keyDelimiter. Refer to API docs
// Example, Given KAFKA_SERVERS = "servers1, server2"
  ConfigSource.fromSystemEnv(keyDelimiter = Some('_'), valueDelimiter = Some(','))


Provide keyDelimiter if you need to consider flattened config as a nested config. Provide valueDelimiter if you need any value to be a list

Example:

Given:

{{{
  property      = "KAFKA.SERVERS" = "server1, server2" ; "KAFKA.SERIALIZERS" = "confluent"
  keyDelimiter   = Some('.')
  valueDelimiter = Some(',')
}}}

then, the below config will work

nested("KAFKA")(string("SERVER") |@| string("FLAG"))(KafkaConfig.apply, KafkaConfig.unapply)

Give valueDelimiter = , and environment with PORT=1222,2221; then reading config yields ListConfig(xyz, List(1222, 2221), postgres)

System Properties

zio-config can source system properties.


val sysPropertiesSource =
  ConfigSource.fromSystemProperties

// If you want to support list of values, then you should be giving a valueDelimiter
val sysPropertiesSourceWithList =
  ConfigSource.fromSystemProperties(None, valueDelimiter = Some(','))

// If you want to consider system-properties as a nested config, provide keyDelimiter. Refer to API doc
// Example, Given KAFKA.SERVERS = "servers1, server2"
  ConfigSource.fromSystemProperties(keyDelimiter = Some('.'), valueDelimiter = Some(','))


Give valueDelimiter = , and environemnt with PORT=1222,2221; then reading config yields ListConfig(xyz, List(1222, 2221), postgres)

Java Properties


val javaProperties: java.util.Properties = new java.util.Properties() // Ideally loaded with values

val javaPropertiesSource =
  ConfigSource.fromProperties(javaProperties)

read(myConfig from javaPropertiesSource)

// If you want to support list of values, then you should be giving a valueDelimiter
val javaPropertiesSourceWithList =
  ConfigSource.fromProperties(javaProperties, valueDelimiter = Some(','))

Properties File


ZConfig.fromPropertiesFile("filepath", myConfig)

// yielding Config[MyConfig] which you provide to
// functions with zio environment as Config[MyConfig]

HOCON String

To enable HOCON source, you have to bring in zio-config-typesafe module. There are many examples in examples module in zio-config.

Here is an quick example


import zio.config.typesafe._, TypesafeConfigSource._
import zio.config.magnolia.DeriveConfigDescriptor._


case class SimpleConfig(port: Int, url: String, region: Option[String])

val automaticDescription = descriptor[SimpleConfig]

val hoconSource =
  TypesafeConfigSource.fromHoconString(
      """
      {
        port : 123
        url  : bla
        region: useast
      }

      """
    )


val anotherHoconSource =
  TypesafeConfigSource.fromHoconString(
      """
        port=123
        url=bla
        region=useast
      """
  )

hoconSource match {
  case Left(value) => Left(value)
  case Right(source) => read(automaticDescription from source)
}

// yielding Right(SimpleConfig(123,bla,Some(useast)))

anotherHoconSource match {
  case Left(value) => Left(value)
  case Right(source) => read(automaticDescription from source)
}

// yielding Right(SimpleConfig(123,bla,Some(useast)))

// Please check other ways to load the hocon file in `TypesafeConfig`

// You could also do, in which case the return type is `Config` service
TypesafeConfig.fromHoconString(
     """
      {
        port : 123
        url  : bla
        region: useast
      }
      """, automaticDescription)


HOCON File

Similar to TypesafeConfig.fromHoconString(str, automaticDescription)


TypesafeConfig.fromHoconFile(new java.io.File("fileapth"), automaticDescription)

Json

You can use zio-config-typesafe module to fetch json as well


val jsonString =
   """
   {
     "port" : "123"
     "url"  : "bla"
     "region": "useast"
   }

   """

TypesafeConfig.fromHoconString(jsonString, automaticDescription)


Please check other ways to load the hocon file in TypesafeConfig

Command Line Arguments

This is currently experimental.

Simple

case class SimpleCommandLineConfig(key1: String, key2: String)

val simpleCmdLineArgs = "--key1 value1 --key2 value2"
val simpleSource = ConfigSource.fromCommandLineArgs(simpleCmdLineArgs.split(' ').toList)
val simpleConfig = descriptor[SimpleCommandLineConfig] from simpleSource

Nested: Approach 1

case class SparkConf(key1: String, key2: String)
case class NestedCommandLineConfig(conf: SparkConf, key3: String)

val nestedCmdLineArgs = "--conf.key1 v1 --conf.key2 v2 --key3 v3"
 
val nestedSource = 
   ConfigSource.fromCommandLineArgs(
   nestedCmdLineArgs.split(' ').toList, 
   keyDelimiter = Some('.')
)

val nestedConfig = descriptor[NestedCommandLineConfig] from nestedSource
assert(read(nestedConfig) == Right(NestedCommandLineConfig(SparkConf("v1", "v2"), "v3")))

This config is for those developers who really used to system properties (-Dconf.key=1) and want to take the same approach towards command line arguments. Here we make use of delimiter . as the tool to nesting.

For those who hate delimited keys in command line arguments and the associated nesting, we will have different approach as given below

Nested: Approach 2


val nestedCmdLineArgs2 = "--conf -key1=v1 --conf -key2=v2 --key3 v3"
val nestedSource2 = ConfigSource.fromCommandLineArgs(nestedCmdLineArgs2.split(' ').toList)
val nestedConfig2 = descriptor[NestedCommandLineConfig] from nestedSource2

assert(read(nestedConfig2) == Right(NestedCommandLineConfig(SparkConf("v1", "v2"), "v3")))

Here we don't use delimiters for nesting, hence keyDelimiter is None. In this case any key-value that comes after --conf comes under the root path conf. This is followed in various places such as SparkConf.

In fact, we can go any level nesting. For example, we can give ---aws --db -url="v" ---aws --kinesis -topic=x, although let's don't complicate our command line arguments.

Map

Both the approaches that we saw with nesting is applicable to map.


val mapArgs = "--conf.key1=value1  --conf.key2=value2"

map("conf")(string) retrieving Map("key1" -> "value1", "key2" -> "value2").

This will also work if mapArgs is --conf -key1=value1 --conf -key2=value2.

List: Approach 1


val listArgs = "--users Jane --users Jack"
val listSource = ConfigSource.fromCommandLineArgs(listArgs.split(' ').toList)
val listConfigCmdLineArgs = list("users")(string) from listSource

assert(read(listConfigCmdLineArgs) == Right(List("Jane", "Jack")))

Lists: Approach 2


val listArgs2 = "--users Jane,Jack"

// args.split(' ') is only for demo purpose. We already get a list if we use zio.App

val listSource2 = ConfigSource.fromCommandLineArgs(
   listArgs2.split(' ').toList,
   valueDelimiter = Some(',')
)

assert(read(list("users")(string) from listSource2) == Right(List("Jane", "Jack")))

Behaviour of List in various sources

No single values will be regarded as list. This is based on feedback from users.

For the config:

Case class Config(key: List[String]) 

If the source is below HOCON (or json)

 {
   Key : value
 }

then it fails, saying a Sequence is expected. This is quite intuitive but worth mentioning for users who are new to HOCON.

However the following configs will work, as it clearly indicate it is a List with square brackets

{
   Key: [value]
}

{
   key: [value1, value2]
}


If the source is a map given below (for example, in system environment), then it succeeds given any delimiter as it contains only one single value.

export key="value"

Given valueDelimiter=Some(',') the following config will work and we are able to retrieve List(value1, value2)

export key="value1, value2"

A Production application config using command line arguments (demo)


case class UserPassword(username: String, password: String)
case class DatabaseConfig(database: UserPassword, url: String)
case class VaultConfig(userPassword: UserPassword)
case class SparkConfig(databaseConfig: DatabaseConfig, numexecs: Int)
case class AppConfig(conf: SparkConfig, vault: VaultConfig, users: List[String], region: List[String])

val complexArgs = "--conf.database.username=Foo --conf.database.password=Bar --conf.database.url=jdbc://xyz --conf.numexecs=10 --vault.username=Foo2 --vault.password=Bar2 --users Jane --users Jack --region TW,US"

val complexSource = ConfigSource.fromCommandLineArgs(
  complexArgs.split(' ').toList,
  Some('.'),
  Some(',')
)
val appConfig = read(descriptor[AppConfig] from complexSource)

Combining multiple sources

There are various ways you can interact with zio-config when dealing with multiple sources. Below given is a naive example, that will help you get a gist of how to get config values from various sources, especially when some of the sources returns ZIO.

import java.io.File

import zio.{App, ExitCode, URIO, ZIO, system}
import zio.config._
import zio.config.typesafe._
import zio.console.{Console, putStrLn}

/**
 * One of the ways you can summon various sources especially
 * when some of the `fromSource` functions return ZIO.
 */
object CombineSourcesExample extends zio.App {
  override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] =
    application.either.flatMap(r => putStrLn(s"Result: ${r}")).exitCode

  final case class Config(username: String , password: String)

  val getDesc: ZIO[system.System, ReadError[String], ConfigDescriptor[Config]] =
    for {
      hoconFile <- ZIO.fromEither(TypesafeConfigSource.fromHoconFile(new File("/invalid/path")))
      constant  <- ZIO.fromEither(TypesafeConfigSource.fromHoconString(s""))
      env       <- ConfigSource.fromSystemEnv
      sysProp   <- ConfigSource.fromSystemProperties
      source    = hoconFile <> constant <> env <> sysProp
    } yield (descriptor[Config] from source)

  val application: ZIO[Console with system.System, String, Unit] =
    for {
      desc        <- getDesc.mapError(_.prettyPrint())
      configValue <- ZIO.fromEither(read(desc)).mapError(_.prettyPrint())
      string      <- ZIO.fromEither(configValue.toJson(desc))
      _ <- putStrLn(string)
    } yield ()
}

Last updated on 1/5/2021 by Afsal Thaj
← Automatic Derivation of ConfigDescriptorUsing ConfigDescriptor for Read, Write, Document and Report →
  • Constant Map
  • Multi Map Source
  • System Environment
  • System Properties
  • Java Properties
  • Properties File
  • HOCON String
  • HOCON File
  • Json
  • Command Line Arguments
    • Simple
    • Nested: Approach 1
    • Nested: Approach 2
    • Map
    • List: Approach 1
    • Lists: Approach 2
    • Behaviour of List in various sources
    • A Production application config using command line arguments (demo)
    • Combining multiple sources
ZIO Config
GitHub
Star
Chat with us on Discord
discord
Additional resources
ZIO HomepageScaladoc
Copyright © 2021 ZIO Maintainers