Groovy ConfigSlurper is covered here with 10 tested examples. Learn configuration management, environment-specific configs, nested properties, and merging on Groovy 5.x.
“Configuration is not just about setting values – it’s about making your application adaptable to any environment without changing a single line of code.”
Martin Fowler, Patterns of Enterprise Architecture
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Intermediate | Reading Time: 16 minutes
Every application needs configuration – database URLs, API keys, feature flags, timeout values. Java developers have leaned on .properties files and YAML for decades, but groovy configslurper offers something far more powerful: a built-in utility that reads configuration scripts written in Groovy DSL syntax, with support for nested structures, environment-specific blocks, and computed values.
Groovy ConfigSlurper is a built-in configuration utility that reads configuration scripts written in Groovy DSL syntax. Unlike flat properties files, ConfigSlurper supports nested structures, environment-specific blocks, closures, computed values, and type-safe access – all without any external dependencies. If you’ve worked with Grails, you’ve already used ConfigSlurper under the hood. Grails’ application.groovy is parsed by ConfigSlurper.
In this post, we’ll walk through 10 practical, tested examples that cover everything from basic usage to advanced patterns like config merging and Grape integration. The examples show exactly when and how to use ConfigSlurper in your Groovy projects. If you need a refresher on Groovy maps and nested data structures, check out our Groovy Map Tutorial first.
Here’s what we’ll cover:
- How ConfigSlurper works internally and what ConfigObject gives you
- 10 hands-on examples from basic to advanced usage
- Head-to-head comparison with Properties and YAML
- Production-tested best practices
- Answers to 5 frequently asked questions
Table of Contents
What Is ConfigSlurper?
groovy.util.ConfigSlurper is a utility class that parses configuration scripts written in Groovy syntax. Instead of flat key-value pairs like .properties files, ConfigSlurper lets you write configuration as a Groovy script with nested blocks, closures, and full access to Groovy language features.
When you parse a configuration script, ConfigSlurper returns a ConfigObject – which is essentially a specialized LinkedHashMap that supports dot-notation access, nested structures, and conversion to flat properties. Here’s what makes it stand out:
- Groovy DSL syntax – configuration files are valid Groovy scripts, so you get IDE support, syntax highlighting, and type safety
- Nested configuration – group related settings using closure blocks instead of repetitive dot-separated prefixes
- Environment support – built-in mechanism for environment-specific overrides (development, test, production)
- Merging – combine multiple configuration sources with later values overriding earlier ones
- Computed values – use closures, method calls, and expressions to derive configuration values at parse time
- No external dependencies – ConfigSlurper is part of Groovy core, available out of the box
Under the hood, ConfigSlurper evaluates the script in a sandboxed binding where property assignments are captured into the ConfigObject. Block closures create nested ConfigObject instances, and the special environments block is parsed separately based on the active environment name.
The ConfigObject class itself extends LinkedHashMap, which means it preserves insertion order and supports all standard Map operations. You can iterate it, convert it to properties, merge it with other ConfigObjects, and serialize it. It also provides a toProperties() method that flattens nested keys into dot-separated strings – useful for passing config to Java libraries that expect java.util.Properties.
One important thing to know before the examples: ConfigSlurper executes Groovy code. This is both its greatest strength (computed values, imports, method calls) and its biggest risk (never parse untrusted input). We’ll cover security considerations in the best practices section.
Here’s a quick look at the basic API surface you’ll be working with:
ConfigSlurper API Overview
// Create a ConfigSlurper (optionally with environment name)
def slurper = new ConfigSlurper()
def prodSlurper = new ConfigSlurper('production')
// Parse a configuration script (returns ConfigObject)
def config = slurper.parse('app.name = "MyApp"')
def config2 = slurper.parse(new File('config.groovy').toURI().toURL())
// ConfigObject key methods
config.toProperties() // flatten to java.util.Properties
config.flatten() // flatten to Map (preserves types)
config.merge(other) // deep-merge another ConfigObject
config.containsKey('k') // check if key exists
With that foundation in place, let’s work through the examples.
10 Tested ConfigSlurper Examples
Let’s get into the code. Each example is self-contained and tested on Groovy 5.x with Java 17+. You can run these in the Groovy console, as standalone scripts, or in a Gradle build.
Example 1: Basic ConfigSlurper Usage
The simplest way to use ConfigSlurper is to parse an inline configuration string. You assign values just like Groovy variables, and access them via dot notation on the returned ConfigObject.
Basic ConfigSlurper Usage
def config = new ConfigSlurper().parse('''
app.name = 'MyGroovyApp'
app.version = '2.1.0'
server.port = 8080
debug = true
''')
println config.app.name // MyGroovyApp
println config.app.version // 2.1.0
println config.server.port // 8080
println config.debug // true
// ConfigObject is a Map - you can iterate it
config.each { key, value ->
println "${key} = ${value}"
}
Output
app = [name:MyGroovyApp, version:2.1.0] server = [port:8080] debug = true
Notice that app.name and app.version are automatically grouped under a nested app key. ConfigSlurper creates the nested structure for you based on the dot-separated keys. The returned ConfigObject behaves like a regular Groovy map – you can iterate, check keys, and access values with bracket notation (config['debug']) or dot notation.
A few things to note about the basic syntax. Values can be strings (single or double-quoted), numbers, booleans, lists, or maps – just like any Groovy assignment. You don’t need equals signs in a specific format or worry about escaping special characters the way you would in a .properties file. The config script is Groovy code, so all Groovy literal syntax is available.
You can also verify the types are preserved:
Type Preservation in ConfigObject
assert config.app.name instanceof String assert config.server.port instanceof Integer assert config.debug instanceof Boolean assert config instanceof groovy.util.ConfigObject assert config instanceof Map
This type preservation is a significant advantage over .properties files, where every value is a String and you need manual parsing for numbers and booleans.
Example 2: Nested Configuration with Closure Blocks
Dot notation works fine for a few settings, but for deeply nested configuration, closure blocks are cleaner and easier to read. This is the idiomatic way to write ConfigSlurper scripts.
Nested Configuration with Closure Blocks
def config = new ConfigSlurper().parse('''
app {
name = 'GroovyCookbook'
version = '3.0.0'
author {
name = 'Rahul'
email = 'rahul@technoscripts.com'
social {
github = 'https://github.com/rahul'
twitter = '@rahul_dev'
}
}
}
database {
host = 'localhost'
port = 5432
name = 'cookbook_db'
credentials {
username = 'admin'
password = 'secret123'
}
}
''')
println config.app.name // GroovyCookbook
println config.app.author.name // Rahul
println config.app.author.social.github // https://github.com/rahul
println config.database.host // localhost
println config.database.credentials.username // admin
// Deep nesting works smoothly
assert config.app.author.social.twitter == '@rahul_dev'
assert config.database.port == 5432
Each closure block creates a new nested ConfigObject. You can nest as deeply as you need – there’s no practical limit. This is much more readable than properties files where you’d write app.author.social.github=https://github.com/rahul repeatedly for every nested key.
You can also mix dot notation and closure blocks in the same config script. This is useful when some sections have many settings (use blocks) and others have just one or two (use dots):
Mixed Dot Notation and Closure Blocks
def mixed = new ConfigSlurper().parse('''
app.name = 'MixedNotation'
database {
host = 'localhost'
port = 5432
}
cache.enabled = true
cache.ttl = 3600
''')
assert mixed.app.name == 'MixedNotation'
assert mixed.database.host == 'localhost'
assert mixed.cache.enabled == true
assert mixed.cache.ttl == 3600
Both approaches produce the same nested ConfigObject structure. Use whichever style makes your specific configuration file most readable. For large config files with many related settings, closure blocks are almost always the better choice.
Example 3: Environment-Specific Configurations
One of ConfigSlurper’s killer features is built-in support for environment-specific configuration. You define an environments block, and ConfigSlurper automatically applies the matching overrides when you specify an environment name. This is the same mechanism Grails uses for its application.groovy configuration.
Environment-Specific Configuration
def configScript = '''
app.name = 'GroovyCookbook'
database {
driver = 'org.postgresql.Driver'
pool.maxSize = 10
}
environments {
development {
database {
url = 'jdbc:postgresql://localhost:5432/dev_db'
pool.maxSize = 5
}
debug = true
}
test {
database {
url = 'jdbc:postgresql://localhost:5432/test_db'
pool.maxSize = 2
}
debug = true
}
production {
database {
url = 'jdbc:postgresql://prod-server:5432/prod_db'
pool.maxSize = 50
}
debug = false
}
}
'''
// Parse with a specific environment
def devConfig = new ConfigSlurper('development').parse(configScript)
println devConfig.database.url // jdbc:postgresql://localhost:5432/dev_db
println devConfig.database.pool.maxSize // 5
println devConfig.database.driver // org.postgresql.Driver
println devConfig.debug // true
def prodConfig = new ConfigSlurper('production').parse(configScript)
println prodConfig.database.url // jdbc:postgresql://prod-server:5432/prod_db
println prodConfig.database.pool.maxSize // 50
println prodConfig.debug // false
// Common settings are always available
assert devConfig.app.name == 'GroovyCookbook'
assert prodConfig.app.name == 'GroovyCookbook'
The pattern here is simple. Settings defined outside the environments block are the defaults. Settings inside a specific environment block override the defaults. You pass the environment name to the ConfigSlurper constructor, and it handles the merging for you. If you parse without an environment argument, the environments block is ignored entirely.
A few important behaviors to be aware of:
- Environment names are arbitrary strings – you’re not limited to “development”, “test”, and “production”. You can use “staging”, “qa”, “local”, or any name that fits your workflow.
- If you specify an environment name that doesn’t have a matching block, you simply get the base configuration with no overrides applied.
- Environment overrides are deep-merged, not replaced. If the base config has
database.driverand the environment block only setsdatabase.url, the driver value is preserved. - You can access the current environment name via
new ConfigSlurper('production').environment– this returns the string “production”.
This pattern eliminates the need for separate configuration files per environment (like config-dev.properties, config-prod.properties) while keeping everything in one readable file.
Example 4: Loading Configuration from a File
In real projects, you’ll store configuration in external files rather than inline strings. ConfigSlurper can parse a URL, a File (via its URL), or a Class that represents a compiled Groovy script. For file-based configurations, you’ll typically work with .groovy files. If you’re new to file I/O in Groovy, see our Groovy File Operations guide.
Loading Configuration from a File
// First, create a config file: app-config.groovy
def configFile = new File('app-config.groovy')
configFile.text = '''
app {
name = 'FileBasedApp'
version = '1.0.0'
}
server {
host = '0.0.0.0'
port = 9090
ssl {
enabled = true
certPath = '/etc/ssl/certs/app.pem'
}
}
logging {
level = 'INFO'
file = '/var/log/app.log'
}
'''
// Parse from file URL
def config = new ConfigSlurper().parse(configFile.toURI().toURL())
println config.app.name // FileBasedApp
println config.server.port // 9090
println config.server.ssl.enabled // true
println config.logging.level // INFO
// You can also parse from a File directly using its URL
assert config.server.host == '0.0.0.0'
assert config.server.ssl.certPath == '/etc/ssl/certs/app.pem'
// Clean up
configFile.delete()
The key method here is parse(URL). You convert a File to a URL using file.toURI().toURL(). This is the standard approach in Groovy applications.
There are several ways to load configuration files depending on your project structure:
Configuration File Loading Methods
// From filesystem (absolute path)
def config1 = new ConfigSlurper().parse(
new File('/etc/myapp/config.groovy').toURI().toURL()
)
// From classpath (common in Gradle/Maven projects)
def config2 = new ConfigSlurper().parse(
getClass().getResource('/config/application.groovy')
)
// From classpath with environment
def config3 = new ConfigSlurper('production').parse(
getClass().getResource('/config/application.groovy')
)
// From a compiled Script class
// If AppConfig.groovy is on the classpath and compiled:
// def config4 = new ConfigSlurper().parse(AppConfig)
The classpath approach (getClass().getResource()) is the most common in application frameworks. Place your .groovy config files in src/main/resources (for Gradle/Maven projects), and they’ll be available on the classpath at runtime. This keeps your configuration bundled with the application while still allowing external overrides.
Example 5: Merging Configurations
Real applications often need to combine configuration from multiple sources – a base config, an environment-specific override, and maybe user-specific preferences. ConfigSlurper’s ConfigObject.merge() method handles this cleanly.
Merging Configuration Sources
def baseConfig = new ConfigSlurper().parse('''
app.name = 'MergeDemo'
database {
host = 'localhost'
port = 5432
pool {
minSize = 2
maxSize = 10
}
}
features {
caching = true
notifications = false
}
''')
def overrideConfig = new ConfigSlurper().parse('''
database {
host = 'prod-db.example.com'
pool {
maxSize = 100
}
}
features {
notifications = true
betaFeature = true
}
monitoring {
enabled = true
endpoint = '/health'
}
''')
// Merge override into base - override values win
baseConfig.merge(overrideConfig)
println baseConfig.app.name // MergeDemo (kept from base)
println baseConfig.database.host // prod-db.example.com (overridden)
println baseConfig.database.port // 5432 (kept from base)
println baseConfig.database.pool.minSize // 2 (kept from base)
println baseConfig.database.pool.maxSize // 100 (overridden)
println baseConfig.features.caching // true (kept from base)
println baseConfig.features.notifications // true (overridden)
println baseConfig.features.betaFeature // true (new from override)
println baseConfig.monitoring.enabled // true (new section from override)
Merging is deep – it recursively combines nested ConfigObject instances. Values from the merged config override existing values, new keys are added, and existing keys not present in the override are preserved. This makes it ideal for layered configuration.
A common real-world pattern is three-layer configuration: defaults, environment, and local overrides:
Three-Layer Configuration Pattern
// Layer 1: Application defaults (shipped with the app)
def defaults = new ConfigSlurper().parse(
getClass().getResource('/config/defaults.groovy')
)
// Layer 2: Environment-specific overrides
def envConfigFile = new File("/etc/myapp/config-${envName}.groovy")
if (envConfigFile.exists()) {
def envOverrides = new ConfigSlurper().parse(envConfigFile.toURI().toURL())
defaults.merge(envOverrides)
}
// Layer 3: Local developer overrides (git-ignored)
def localFile = new File('config-local.groovy')
if (localFile.exists()) {
def localOverrides = new ConfigSlurper().parse(localFile.toURI().toURL())
defaults.merge(localOverrides)
}
// defaults now contains the fully merged configuration
def finalConfig = defaults
This pattern gives you sensible defaults, environment-specific tuning, and the ability for individual developers to override settings locally without affecting the team. The key is that merge() mutates the receiver – baseConfig.merge(override) modifies baseConfig in place and returns it.
Example 6: Config with Closures and Computed Values
Since ConfigSlurper scripts are valid Groovy code, you can use closures, string interpolation, method calls, and expressions to compute configuration values dynamically. This is where ConfigSlurper becomes far more powerful than any static configuration format.
Closures and Computed Values
def config = new ConfigSlurper().parse('''
app {
name = 'ComputedConfig'
version = '2.0.0'
fullName = "${name} v${version}"
}
server {
host = 'api.example.com'
port = 443
protocol = 'https'
baseUrl = "${protocol}://${host}:${port}"
}
paths {
base = '/opt/myapp'
logs = "${base}/logs"
data = "${base}/data"
config = "${base}/config"
temp = System.getProperty('java.io.tmpdir') ?: '/tmp'
}
runtime {
cores = Runtime.runtime.availableProcessors()
threadPool = cores * 2
maxMemoryMB = (Runtime.runtime.maxMemory() / (1024 * 1024)) as int
}
''')
println config.app.fullName // ComputedConfig v2.0.0
println config.server.baseUrl // https://api.example.com:443
println config.paths.logs // /opt/myapp/logs
println config.paths.temp // system temp directory
println config.runtime.cores // number of CPU cores
println config.runtime.threadPool // cores * 2
assert config.paths.data == '/opt/myapp/data'
assert config.runtime.threadPool == config.runtime.cores * 2
This is where ConfigSlurper truly shines compared to plain properties files. You can reference other configuration values using GString interpolation, call system methods, and even perform arithmetic. The interpolation happens within the same closure scope, so ${name} inside the app block refers to the name variable defined earlier in that same block.
Here are a few more patterns for computed values that you’ll encounter in real projects:
Advanced Computed Configuration Patterns
def config = new ConfigSlurper().parse('''
// Date-based configuration
app {
buildDate = new Date().format('yyyy-MM-dd')
startYear = 2024
}
// Conditional configuration
server {
port = System.getenv('PORT') ? System.getenv('PORT') as int : 8080
workers = Math.max(2, Runtime.runtime.availableProcessors() - 1)
}
// Using Groovy collections
security {
allowedIPs = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
blockedPaths = ['/admin', '/internal', '/debug'].collect { it + '/**' }
}
''')
println config.app.buildDate // 2026-03-10 (today's date)
println config.server.workers // CPU cores - 1 (min 2)
println config.security.blockedPaths // [/admin/**, /internal/**, /debug/**]
However, be cautious with side effects in configuration scripts. Keep computation pure and predictable. Avoid making network calls, writing files, or modifying global state from within config scripts. If you need to fetch configuration from a remote source, do that in your application startup code and merge the results into your ConfigObject.
Example 7: Converting ConfigObject to Properties and Map
Sometimes you need to convert a ConfigObject to a flat java.util.Properties object (for Java library compatibility) or a plain Map. ConfigSlurper handles both conversions natively. This is particularly useful when integrating with Java frameworks that expect standard Properties objects.
Converting ConfigObject to Properties
def config = new ConfigSlurper().parse('''
app {
name = 'ConversionDemo'
version = '1.5.0'
}
database {
host = 'localhost'
port = 5432
credentials {
username = 'dbuser'
password = 'dbpass'
}
}
features {
caching = true
logging = false
}
''')
// Convert to flat Properties (dot-separated keys)
def props = config.toProperties()
println props.getClass() // java.util.Properties
println props['app.name'] // ConversionDemo
println props['database.credentials.username'] // dbuser
println props['database.port'] // 5432
// All keys are flattened with dot notation
props.each { key, value ->
println " ${key} = ${value}"
}
Output
app.name = ConversionDemo app.version = 1.5.0 database.host = localhost database.port = 5432 database.credentials.username = dbuser database.credentials.password = dbpass features.caching = true features.logging = false
The toProperties() method flattens nested keys using dot notation and converts all values to strings – exactly what java.util.Properties expects. This is the method to use when you need to pass configuration to Java APIs.
For cases where you need a flat map but want to preserve the original value types (integers stay integers, booleans stay booleans), use flatten():
Flatten and Map Conversion
// Convert to a plain nested Map (flatten the ConfigObject)
def map = config.flatten()
println map.getClass() // java.util.LinkedHashMap
println map['database.host'] // localhost
println map['features.caching'] // true (Boolean, not String)
println map['database.port'] // 5432 (Integer, not String)
// Compare: toProperties() converts everything to String
def propsPort = config.toProperties()['database.port']
println propsPort.getClass() // java.lang.String
def flatPort = config.flatten()['database.port']
println flatPort.getClass() // java.lang.Integer
// ConfigObject itself already implements Map
assert config instanceof Map
assert config.database instanceof Map
// You can also write a ConfigObject back to a Properties file
def propsFile = new File('output.properties')
config.toProperties().store(propsFile.newWriter(), 'Generated config')
println propsFile.text
propsFile.delete()
Use toProperties() when interfacing with Java APIs that require Properties. Use flatten() when you need a flat map within Groovy code and want to keep type information. Use the nested ConfigObject directly when working within Groovy – it’s the most natural and powerful representation.
Example 8: Default Values and Missing Keys
One of the common pain points with configuration is handling missing keys gracefully. ConfigSlurper has a specific behavior for missing keys that you need to understand to avoid subtle bugs.
Default Values and Missing Keys
def config = new ConfigSlurper().parse('''
app.name = 'DefaultsDemo'
database.host = 'localhost'
''')
// Accessing a missing key returns an empty ConfigObject, NOT null
def missingValue = config.database.port
println missingValue // [:]
println missingValue.getClass() // class groovy.util.ConfigObject
println missingValue.isEmpty() // true
// This means truthy/falsy checks work correctly
if (!config.database.port) {
println 'Port is not configured' // This prints
}
// IMPORTANT: checking == null does NOT work!
println (config.database.port == null) // false - it's an empty ConfigObject, not null
// Using the Elvis operator for defaults
def port = config.database.port ?: 5432
println port // 5432
// Using get() with a default value
def timeout = config.get('server.timeout', 30000)
println timeout // 30000
The main point here is that accessing a missing key in ConfigObject never throws an exception and never returns null. Instead, it returns an empty ConfigObject, which is falsy in Groovy’s truth semantics. This means the Elvis operator (?:) works perfectly for providing defaults.
For explicit existence checks, use containsKey():
Safe Patterns for Missing Keys
// Safe pattern: check before access
if (config.database.host) {
println "Host: ${config.database.host}" // Host: localhost
}
// Using containsKey for explicit checks
println config.containsKey('app') // true
println config.containsKey('server') // false
println config.database.containsKey('host') // true
println config.database.containsKey('port') // false
// Providing defaults via merge - the cleanest approach
def defaults = new ConfigSlurper().parse('''
database {
host = 'localhost'
port = 5432
pool.maxSize = 10
}
server {
port = 8080
timeout = 30000
}
''')
defaults.merge(config) // User config overrides defaults
println defaults.database.host // localhost (from user config)
println defaults.database.port // 5432 (from defaults)
println defaults.server.port // 8080 (from defaults)
println defaults.server.timeout // 30000 (from defaults)
The merge-based defaults pattern is the most reliable approach for production applications. Define all your expected keys with sensible defaults in a base config, then merge user/environment configuration over it. This guarantees that every key has a value, eliminates the need for Elvis operators scattered throughout your code, and makes the full set of available configuration options discoverable in one place.
One gotcha to watch out for: if you access a deeply nested missing key like config.server.ssl.keystore.path, ConfigSlurper creates empty nested ConfigObjects at each level. While these are all falsy, they do add empty map entries to the parent ConfigObject. This usually doesn’t cause problems, but it’s worth knowing about if you’re iterating config keys.
Example 9: Config with Lists and Complex Structures
ConfigSlurper is not limited to simple key-value pairs. You can store lists, maps, ranges, and any Groovy data structure as configuration values. This makes it suitable for complex application setups that would be awkward in flat properties files.
Lists and Complex Structures
def config = new ConfigSlurper().parse('''
app.name = 'ComplexConfig'
// Lists as values
server {
hosts = ['web1.example.com', 'web2.example.com', 'web3.example.com']
ports = [8080, 8081, 8082]
allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
'http://localhost:3000'
]
}
// Maps as values
database {
connections = [
primary: [host: 'db1.example.com', port: 5432],
replica: [host: 'db2.example.com', port: 5432],
analytics: [host: 'db3.example.com', port: 5433]
]
}
// List of maps
users {
admins = [
[name: 'Alice', role: 'super_admin', email: 'alice@example.com'],
[name: 'Bob', role: 'admin', email: 'bob@example.com']
]
}
// Ranges
validation {
portRange = 1024..65535
acceptableResponseCodes = [200, 201, 202, 204]
}
''')
// Access lists
println config.server.hosts // [web1.example.com, web2.example.com, web3.example.com]
println config.server.hosts[0] // web1.example.com
println config.server.hosts.size() // 3
// Access nested maps within config
println config.database.connections.primary.host // db1.example.com
println config.database.connections.replica.port // 5432
// Iterate list of maps
config.users.admins.each { admin ->
println "${admin.name} (${admin.role}) - ${admin.email}"
}
// Ranges work too
assert 8080 in config.validation.portRange
assert 80 !in config.validation.portRange
Output
Alice (super_admin) - alice@example.com Bob (admin) - bob@example.com
Lists and maps in ConfigSlurper behave exactly as they do in regular Groovy code. You get full access to GDK methods like findAll, collect, each, and groupBy on the values:
Filter and Transform Configuration Data
// Filter and transform configuration data
def readOnlyDbs = config.database.connections.findAll { name, details ->
name != 'primary'
}
println readOnlyDbs.keySet() // [replica, analytics]
// Find all HTTPS origins
def secureOrigins = config.server.allowedOrigins.findAll {
it.startsWith('https')
}
println secureOrigins // [https://app.example.com, https://admin.example.com]
// Get admin emails as a list
def adminEmails = config.users.admins.collect { it.email }
println adminEmails // [alice@example.com, bob@example.com]
// Check if a port is in the valid range
def isValid = { port -> port in config.validation.portRange }
assert isValid(8080)
assert !isValid(80)
This makes ConfigSlurper ideal for complex configuration scenarios like multi-database setups, load balancer host lists, role-based access control, or multi-tenant applications. The full power of Groovy collections is available on your configuration data.
One thing to note: plain maps defined as values ([key: value] syntax) are regular LinkedHashMap instances, not ConfigObject instances. This means they won’t have the “empty ConfigObject for missing keys” behavior. If you access a missing key on a plain map, you’ll get null. Keep this distinction in mind when mixing nested blocks (which create ConfigObjects) with inline maps (which create plain Maps).
Example 10: Using ConfigSlurper with Grape and External Dependencies
Since ConfigSlurper scripts are full Groovy scripts, you can use @Grab annotations to pull in external dependencies and use them in your configuration. This is powerful for advanced scenarios like encrypting secrets, fetching config from remote sources, or validating configuration at parse time. For more on Grape, see our Groovy Grape guide.
Environment Variables with Defaults
// Example: Config script that reads environment variables
// and provides sensible defaults - no external deps needed
def config = new ConfigSlurper().parse('''
app {
name = 'GrapeConfigDemo'
env = System.getenv('APP_ENV') ?: 'development'
}
database {
host = System.getenv('DB_HOST') ?: 'localhost'
port = (System.getenv('DB_PORT') ?: '5432') as int
name = System.getenv('DB_NAME') ?: 'myapp_dev'
username = System.getenv('DB_USER') ?: 'devuser'
password = System.getenv('DB_PASS') ?: 'devpass'
url = "jdbc:postgresql://${host}:${port}/${name}"
}
''')
println config.app.name // GrapeConfigDemo
println config.app.env // development (or APP_ENV value)
println config.database.url // jdbc:postgresql://localhost:5432/myapp_dev
This pattern of reading environment variables with fallback defaults is extremely common in cloud-native applications and twelve-factor apps. ConfigSlurper makes it clean and readable.
For Grape integration, here’s what a real-world config script with external dependencies looks like:
Config Script with Grape Dependencies
// config-with-grape.groovy - standalone config file
// Uncomment @Grab lines to use in real projects
// @Grab('org.apache.groovy:groovy-json:4.0.18')
// import groovy.json.JsonSlurper
// Fetch remote config and merge it
// remoteConfig = new JsonSlurper().parse(
// new URL('https://config-server.example.com/api/v1/config/myapp')
// )
// @Grab('commons-codec:commons-codec:1.16.0')
// import org.apache.commons.codec.binary.Base64
// encodedSecret = new String(Base64.decodeBase64(System.getenv('ENCODED_SECRET')))
And the practical pattern for environment-aware configuration loading:
Environment-Aware Configuration Loading
// Practical pattern: load config with env-aware settings
def envName = System.getenv('APP_ENV') ?: 'development'
def envConfig = new ConfigSlurper(envName).parse('''
app.name = 'ProductionReady'
app.logLevel = 'INFO'
cache {
provider = 'local'
ttlSeconds = 300
}
environments {
development {
app.logLevel = 'DEBUG'
app.devMode = true
cache {
provider = 'local'
ttlSeconds = 10
}
}
test {
app.logLevel = 'WARN'
app.devMode = false
cache {
provider = 'local'
ttlSeconds = 0
}
}
production {
app.logLevel = 'WARN'
app.devMode = false
cache {
provider = 'redis'
host = System.getenv('REDIS_HOST') ?: 'redis.internal'
ttlSeconds = 3600
}
}
}
''')
println envConfig.app.name // ProductionReady
println envConfig.app.logLevel // DEBUG (when APP_ENV is unset)
println envConfig.cache.provider // local (when APP_ENV is unset)
The Grape integration opens up possibilities like fetching configuration from a remote config server (Spring Cloud Config, Consul, etcd), decrypting secrets with a crypto library, or validating configuration schemas at parse time. For JSON-based config integration, check our Groovy JSON Parsing guide.
A word of caution: while Grape in config scripts is powerful, it adds startup latency (dependency resolution) and network dependencies. For production applications, prefer pre-resolved dependencies via Gradle or Maven, and use config scripts purely for setting values. Reserve Grape for quick scripts and prototypes.
ConfigSlurper vs Properties vs YAML
If you’re choosing between configuration approaches in a Groovy project, here’s how they compare across the dimensions that matter most.
| Feature | ConfigSlurper | Properties | YAML (SnakeYAML) |
|---|---|---|---|
| Syntax | Groovy DSL | key=value | Indentation-based |
| Nested structures | Native closure blocks | Dot-separated keys | Native indentation |
| Environment support | Built-in | Manual (separate files) | Manual (Spring profiles) |
| Computed values | Full Groovy expressions | No | No (without Spring EL) |
| Type safety | Values keep original types | Everything is String | Auto-typed |
| Merging | Built-in merge() | Manual | Manual |
| IDE support | Full Groovy IDE support | Basic | Good with plugins |
| External dependencies | None (Groovy core) | None (Java core) | SnakeYAML library |
| Security | Executes Groovy code | Passive parsing | Passive parsing |
| Best for | Groovy/Grails projects | Simple Java apps | Spring Boot, Kubernetes |
Let’s break down the practical implications of this comparison.
ConfigSlurper Strengths
When to use ConfigSlurper: You’re building a Groovy or Grails application, you need environment-specific configs without multiple files, you want computed values or conditional logic in your config, and you trust the configuration source (since it executes Groovy code). ConfigSlurper is also the natural choice for Gradle build scripts and Groovy-based automation tools.
The biggest advantage is expressiveness. In a properties file, you can’t compute a database URL from its components. In ConfigSlurper, url = "jdbc:postgresql://${host}:${port}/${name}" is natural. You also get type preservation – port = 5432 stays an integer, debug = true stays a boolean. No more Integer.parseInt(props.getProperty("port")) boilerplate.
Properties Strengths
When to use Properties: You need the simplest possible configuration format, your config is flat key-value pairs with no nesting, you need cross-language compatibility (Properties is supported everywhere), or you’re accepting configuration from untrusted sources. Properties files are also the standard for Java resource bundles and internationalization.
YAML Strengths
When to use YAML: You’re working with Spring Boot (which uses YAML natively), you need configuration that’s readable by tools in other languages (Kubernetes, Docker, Ansible), or your team prefers the visual hierarchy of indentation-based syntax. YAML is also the de facto standard for infrastructure-as-code tools.
When to Avoid ConfigSlurper
When to avoid ConfigSlurper: You’re accepting configuration from untrusted users (security risk – it executes arbitrary Groovy code), you need language-agnostic config that multiple services in different languages will read (YAML or JSON is better), or your team is not familiar with Groovy syntax. Also avoid it for configuration that will be edited by non-developers – YAML or simple properties are more approachable for operations staff.
Here’s a quick side-by-side showing the same configuration in all three formats, so you can see the ergonomic differences:
ConfigSlurper Format (.groovy)
// ConfigSlurper (.groovy)
database {
host = 'localhost'
port = 5432
credentials {
username = 'admin'
password = System.getenv('DB_PASS') ?: 'dev123'
}
}
Properties Format (.properties)
# Properties (.properties) database.host=localhost database.port=5432 database.credentials.username=admin database.credentials.password=dev123
YAML Format (.yml)
# YAML (.yml)
database:
host: localhost
port: 5432
credentials:
username: admin
password: dev123
Notice that only the ConfigSlurper version can dynamically read an environment variable with a fallback. The Properties and YAML versions require separate mechanisms (like Spring’s ${DB_PASS:dev123} placeholder syntax) to achieve the same result. This dynamic capability is ConfigSlurper’s defining advantage – and its defining risk.
Best Practices
After years of using ConfigSlurper in production Groovy and Grails applications, here are the practices that consistently pay off.
Structure and Style
- Use closure blocks over dot notation for groups –
database { host = 'localhost'; port = 5432 }is more readable thandatabase.host = 'localhost'plusdatabase.port = 5432when you have multiple related settings. Reserve dot notation for single standalone settings likeapp.name = 'MyApp'. - Keep configuration files pure – avoid heavy computation, I/O operations, or side effects in config scripts. Config files should set values, not run business logic. If you need complex initialization, do it in your application startup code after reading the config.
- Organize by concern, not by type – group settings by what they configure (database, server, cache), not by their data type. This makes it easy to find and update related settings together.
- Comment generously – use comments in your config files to explain what each setting does, what values are acceptable, and what the default means. Future you (and your teammates) will thank you.
Configuration Architecture
- Layer your configuration – use a defaults file, an environment-specific file, and optionally a local override file. Merge them in order with
merge(). This gives you a single place to see all available settings (defaults), environment tuning (overrides), and developer convenience (local). - Validate configuration early – check required keys at application startup and fail fast with clear error messages if critical configuration is missing. Don’t wait for the first database connection to discover the URL is not set.
- Use the environments block for environment switching – don’t create separate config files per environment unless you have a specific reason. The built-in
environmentsblock keeps everything in one file and makes differences between environments immediately visible. - Use toProperties() for Java interop – when passing config to Java libraries that expect
Properties, usetoProperties()rather than manually extracting values. This handles the flattening and type conversion for you.
Security
- Never hardcode secrets – use environment variables (
System.getenv()) or a secrets manager for passwords, API keys, and tokens. Never commit secrets in config files to version control. Even for development, use aconfig-local.groovyfile that is.gitignored. - Be cautious with security – ConfigSlurper executes Groovy code. Never parse configuration from untrusted sources without proper sandboxing. If users can upload or modify config files, use a passive format like JSON or Properties instead.
- Restrict file permissions – config files that contain sensitive information (even environment variable references) should have restricted file system permissions. On Unix systems, use
chmod 600for files that only the application user should read. - Audit config changes – treat configuration files like code. Keep them in version control, review changes in pull requests, and maintain a changelog for production configuration updates.
Conclusion
Groovy ConfigSlurper is one of those tools that, once you start using, you wonder how you ever managed without. It takes the tedious, error-prone world of configuration management and turns it into clean, expressive, type-safe Groovy code. You get nested structures without XML verbosity, environment switching without framework magic, computed values without template engines, and merging without manual Map gymnastics.
We covered 10 examples spanning the full spectrum of ConfigSlurper usage:
- Examples 1-2: Basic usage and nested configuration with dot notation and closure blocks
- Example 3: Environment-specific configurations with the built-in
environmentsblock - Example 4: Loading configuration from external files and the classpath
- Example 5: Merging multiple configuration sources with layered overrides
- Example 6: Computed values, string interpolation, and dynamic configuration
- Example 7: Converting ConfigObject to Properties and flat Maps for Java interop
- Example 8: Handling missing keys and providing defaults safely
- Examples 9-10: Complex data structures, Grape integration, and environment variable patterns
That’s a solid toolkit for any Groovy project – from quick automation scripts to enterprise microservices.
The one caveat worth repeating: ConfigSlurper executes Groovy code. This is a feature when you control the config source, and a security risk when you don’t. For untrusted input, stick with Properties, JSON, or YAML. For everything else, ConfigSlurper is the most expressive and productive configuration tool in the Groovy ecosystem.
If you want to explore related topics, our guides on Groovy File Operations and Groovy JSON Parsing pair well with ConfigSlurper for building solid configuration pipelines. And if you’re building applications that need to manage external dependencies in scripts, check out our Groovy Grape guide.
Up next: Groovy Logging – Complete Guide
Frequently Asked Questions
What is Groovy ConfigSlurper and when should I use it?
ConfigSlurper is a built-in Groovy utility (groovy.util.ConfigSlurper) that parses configuration scripts written in Groovy DSL syntax. It returns a ConfigObject (a specialized Map) that supports dot-notation access, nested structures, environment-specific blocks, and merging. Use it when you’re building Groovy or Grails applications and need expressive, type-safe configuration with features like computed values and environment switching. Avoid it for untrusted config sources since it executes Groovy code.
How does ConfigSlurper handle environment-specific configuration?
ConfigSlurper has built-in environment support. You define an environments block in your config script with sub-blocks for each environment (e.g., development, test, production). When you create a ConfigSlurper with an environment name – new ConfigSlurper('production') – it automatically merges the matching environment block over the base configuration. Settings outside the environments block serve as defaults, and environment-specific settings override them. If no environment is specified, the environments block is ignored.
What happens when I access a missing key in ConfigObject?
Accessing a missing key in ConfigObject returns an empty ConfigObject (which prints as [:]), not null. This empty ConfigObject is falsy in Groovy’s truth semantics, so you can use the Elvis operator (config.missingKey ?: 'default') for defaults. For explicit existence checks, use config.containsKey('keyName'). This behavior prevents NullPointerException but can create subtle bugs if you check == null instead of using truthiness.
Can I convert ConfigObject to Java Properties?
Yes. Call config.toProperties() to convert a ConfigObject to a flat java.util.Properties instance. Nested keys are flattened using dot notation (e.g., database.credentials.username), and all values are converted to strings. For a flat map that preserves original value types (integers stay integers, booleans stay booleans), use config.flatten() instead. Both methods are useful when passing configuration to Java libraries that expect Properties or flat Map objects.
Is ConfigSlurper safe to use with untrusted configuration files?
No. ConfigSlurper executes the configuration file as a Groovy script, which means it can run arbitrary code including file system operations, network calls, and system commands. Never use ConfigSlurper to parse configuration from untrusted sources (user uploads, external APIs, etc.) without proper sandboxing. For untrusted input, use passive parsers like java.util.Properties, JSON (JsonSlurper), or YAML (SnakeYAML) that do not execute code during parsing.
Related Posts
Previous in Series: Groovy DataSet – Type-Safe Database Queries
Next in Series: Groovy Logging – Complete Guide
Related Topics You Might Like:
- Groovy File Operations – Reading, Writing, and More
- Groovy JSON Parsing – Complete Guide
- Groovy Grape – Dependency Management
This post is part of the Groovy Cookbook series on TechnoScripts.com

No comment