Getting Started with ZIO Config
The library aims to have a powerful & purely functional, yet a thin interface to access configuration information inside an application. There are many examples in here straight away as well.
Note that this documentation is for 1.x series. For newer versions, please refer to docs section in GitHub.
Adding dependency
If you are using sbt:
libraryDependencies += "dev.zio" %% "zio-config" % 3.0.4
Optional Dependency with magnolia module (Auto derivation)
libraryDependencies += "dev.zio" %% "zio-config-magnolia" % 3.0.4
Optional Dependency with refined module (Integration with refined library)
libraryDependencies += "dev.zio" %% "zio-config-refined" % 3.0.4
Optional Dependency with typesafe module (HOCON/Json source)
libraryDependencies += "dev.zio" %% "zio-config-typesafe" % 3.0.4
Optional Dependency with yaml module (Yaml source)
libraryDependencies += "dev.zio" %% "zio-config-yaml" % 3.0.4
Optional Dependency for a random generation of a config
libraryDependencies += "dev.zio" %% "zio-config-gen" % 3.0.4
Describe the config by hand
We must fetch the configuration from the environment to a case class (product) in scala. Let it be MyConfig
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: ConfigDescriptor[MyConfig] =
(string("LDAP") zip int("PORT") zip string("DB_URL")).to[MyConfig]
// ConfigDescriptor[ MyConfig]
To get a tuple,
val myConfigTupled: ConfigDescriptor[(String, Int, String)] =
(string("LDAP") zip int("PORT") zip string("DB_URL"))
Fully automated Config Description
If you don't like describing your configuration manually, and rely on the names of the parameter in the case class (or sealed trait),
there is a separate module called zio-config-magnolia
.
Note: zio-config-shapeless
is an alternative to zio-config-magnolia
to support scala 2.11 projects.
It will be deprecated once we find users have moved on from scala 2.11.
import zio.config._
import zio.config.magnolia.{Descriptor, descriptor}
val myConfigAutomatic = descriptor[MyConfig]
myConfig
and myConfigAutomatic
are same description, and is of the same type.
Refer to API docs for more explanations on descriptor More examples on automatic derivation is in examples module of zio-config
Read config from various sources
There are more information on various sources in here.
Below given is a simple example.
val map =
Map(
"LDAP" -> "xyz",
"PORT" -> "8888",
"DB_URL" -> "postgres"
)
val source = ConfigSource.fromMap(map)
read(myConfig from source)
// Alternatively, you can rely on `Config.from..` pattern to get ZLayers.
val result =
ZConfig.fromMap(map, myConfig)
// Layer[ReadError[String], Config[A]]
You can run this to completion as in any zio application.
How to use config descriptor
Readers from configdescriptor
As mentioned before, you can use config descriptor to read from various sources.
val anotherResult =
read(myConfig from source)
Note that, this is almost similar to Config.fromMap(map, myConfig)
in the previous section.
More details in here.
Documentations using ConfigDescriptor
generateDocs(myConfig)
//Creates documentation (automatic)
val betterConfig =
(string("LDAP") ?? "Related to auth" zip int("PORT") ?? "Database port" zip
string("DB_URL") ?? "url of database"
).to[MyConfig]
generateDocs(betterConfig).toTable.toGithubFlavouredMarkdown
// Custom documentation along with auto generated docs
More details in here.
Writers from ConfigDescriptor
write(myConfig, MyConfig("xyz", 8888, "postgres")).map(_.flattenString())
// Map("LDAP" -> "xyz", "PORT" -> "8888", "DB_URL" -> "postgres")
More details in here.
Report generation from ConfigDescriptor
generateReport(myConfig, MyConfig("xyz", 8888, "postgres"))
// Generates documentation showing value of each parameter
Generate a random config
import zio.config.derivation.name
import zio.config.magnolia._, zio.config.gen._
object RandomConfigGenerationSimpleExample extends App {
sealed trait Region
@name("ap-southeast-2")
case object ApSouthEast2 extends Region
@name("us-east")
case object UsEast extends Region
case class DetailsConfig(username: String, region: Region)
println(generateConfigJson(descriptor[DetailsConfig]).unsafeRunChunk)
// yields for example
// Chunk(
// {
// "region" : "ap-southeast-2",
// "username" : "eU2KlfATwYZ5s0Y"
// }
// )
}
Accumulating all errors
For any misconfiguration, the ReadError collects all of them with proper semantics: AndErrors
and OrErrors
.
Instead of directly printing misconfigurations, the ReadError.prettyPrint
shows the path, detail of collected misconfigurations.
- All misconfigurations of
AndErrors
are put in parallel lines.
╥
╠══╗
║ ║ FormatError
║ MissingValue
OrErrors
are in the same line which indicates a sequential misconfiguration
╥
╠MissingValue
║
╠FormatError
Here is a complete example:
ReadError:
╥
╠══╦══╗
║ ║ ║
║ ║ ╠─MissingValue
║ ║ ║ path: var2
║ ║ ║ Details: value of type string
║ ║ ║
║ ║ ╠─MissingValue path: envvar3
║ ║ ║ path: var3
║ ║ ║ Details: value of type string
║ ║ ║
║ ║ ▼
║ ║
║ ╠─FormatError
║ ║ cause: Provided value is wrong, expecting the type int
║ ║ path: var1
║ ▼
▼
It says, fix FormatError
related to path "var1" in the source. For the next error, either provide var2 or var3
to fix MissingValue
error.
Note: Use prettyPrint method to avoid having to avoid seeing highly nested ReadErrors, that can be difficult to read.
Config is your ZIO environment
Take a look at the below example that shows an entire mini app.
This will tell you how to consider configuration as just a part of Environment
of your ZIO functions across your application.
import zio._
case class ApplicationConfig(bridgeIp: String, userName: String)
val configuration =
(string("bridgeIp") zip string("username")).to[ApplicationConfig]
val finalExecution =
for {
appConfig <- getConfig[ApplicationConfig]
_ <- Console.printLine(appConfig.bridgeIp)
_ <- Console.printLine(appConfig.userName)
} yield ()
val configLayer = ZConfig.fromPropertiesFile("file-location", configuration)
// Main App
val pgm = finalExecution.provideLayer(configLayer)
Separation of concerns with narrow
In bigger apps you can have a lot of components and, consequently - a lot of configuration fields. It's not ideal to pass around the whole configuration object as a dependency for all of those components: this way you break the separation of concerns principle. Component should be aware only about dependencies it cares about and uses somehow.
So to avoid that and do it in an ergonomic way, there's a narrow
syntax extension for ZLayer
, available under import zio.config.syntax._
import:
import zio._
import zio.config.typesafe._
import zio.config.syntax._
import zio.config.magnolia._
trait Endpoint
trait Repository
case class AppConfig(api: ApiConfig, db: DbConfig)
case class DbConfig (url: String, driver: String)
case class ApiConfig(host: String, port: Int)
val configDescription = descriptor[AppConfig]
// components have only required dependencies
val endpoint: ZLayer[ApiConfig, Nothing, Endpoint] = ZLayer.succeed(new Endpoint {})
val repository: ZLayer[DbConfig, Nothing, Repository] = ZLayer.succeed(new Repository {})
val cfg = TypesafeConfig.fromResourcePath(configDescription)
cfg.narrow(_.api) >>> endpoint // narrowing down to a proper config subtree
cfg.narrow(_.db) >>> repository
What's new in zio-config-3.x ?
Some of these details are repeated in certain parts of the documentations. We thought we will repeat here, which is much better than readers missing it out.
Removed DeriveConfigDescriptor
and SealedTraitStrategy
DeriveConfigDescriptor
used to be the interface where users can override certain default behaviours of automatic derivation, mainly to change the way zio-config handles custom key names, and coproducts (sealed traits). Now this is deleted forever.
Take a look at the API docs of descriptor
, descriptorForPureConfig
, descriptorWithClassNames
and descriptorWithoutClassNames
for more information.
Custom keys is just about changing ConfigDescriptor
We recommend users to make use of mapKey
in ConfigDescriptor
to change any behaviour of the field-names (or class names, or sealed-trait names). The release ensures we no longer need to extend an interface called DeriveConfigDescriptor
to change this behaviour.
Example:
Now on, the only way to change keys is as follows:
// mapKey is just a function in `ConfigDescriptor` that pre-existed
val config = descriptor[Config].mapKey(_.toUpperCase)
instead of
// No longer supported
val customDerivation = new DeriveConfigDescriptor {
override def mapFieldName(key: String) = key.toUpperCase
}
import customDerivation._
val config = descriptor[Config]
Inbuilt support for pure-config
Many users make use of the label type
in HOCON files to annotate the type of the coproduct.
Now on, zio-config has inbuilt support for reading such a file/string using descriptorForPureConfig
.
import zio.config._, typesafe._, magnolia._
sealed trait X
case class A(name: String) extends X
case class B(age: Int) extends X
case class AppConfig(x: X)
val str =
s"""
x : {
type = A
name = jon
}
"""
read(descriptorForPureConfig[AppConfig] from ConfigSource.fromHoconString(str))
Allow concise config source strings
With this release we have descriptorWithoutClassNames
along with descriptor
that just completely discards the name of the sealed-trait and sub-class (case-class) names, allowing your source less verbose. Note that unlike pure-config
example above, we don't need to have an extra label type : A
.
sealed trait Y
object Y {
case class A(age: Int) extends Y
case class B(name: String) extends Y
}
case class AppConfig(x: Y)
val str =
s"""
x : {
age : 10
}
"""
read(descriptorWithoutClassNames[AppConfig] from ConfigSource.fromHoconString(str))
PS: If you are using descriptor
instead of descriptorWithoutClassNames
, then the source has to be:
x : {
A : {
age : 10
}
}
Your ConfigSource is exactly your product and coproduct
Some users prefer to encode the config-source exactly the same as that of Scala class files. The implication is, the source will know the name of the sealed trait
and the name of all of its subclasses
. There are several advantages to such an approach, while it can be questionable in certain situations. Regardless, zio-config now has inbuilt support to have this pattern.
Example:
Say, the config ADT is as below:
sealed trait Y
object Y {
case class A(age: Int) extends Y
case class B(name: String) extends Y
}
case class AppConfig(x: X)
Then the corresponding config-source should be as follows. Keep a note that under x
, the name of sealed trait Y
also exist.
val str =
s"""
x : {
Y : {
A : {
age : 10
}
}
}
"""
To read such a string (or any config-source encoded in such a hierarchy), use descriptorWithClassNames
instead of descriptor
. In short, descriptorWithClassNames
considers the name of sealed-trait.
read(descriptorWithClassNames[AppConfig] from ConfigSource.fromHoconString(str))
More composable Descriptor
The whole bunch of methods such as descriptor
works with the type class Descriptor
. You can summon a Descriptor
for type A
using Descriptor[A].apply
, which will give you access to lower level methods such as removeSubClassNameKey
. These methods directly exist in ConfigDescriptor
, however inaccessible, since there is no guarantee that a manually created ConfigDescriptor
correctly tags keys to its types (i.e, a particular key is the name of a sub-class of a sealed-trait)
case class A (...)
val config1: Descriptor[A] = Descriptor[A].removeSealedTraitNameKey
val config2: Descriptor[A] = Descriptor[A].removeSubClassNameKey
// similar to descriptorWithoutClassNames
val config3: Descriptor[A] = Descriptor[A].removeSealedTraitNameKey. removeSubClassNameKey.mapFieldName(_.toUpperCase)