Data from files, APIs and databases is rarely complete: a customer without an address, an order without lines, a field the partner forgot. In Java, every missing piece risks a NullPointerException. Groovy gives you operators that make missing data harmless — the same problem Java’s Optional solves, with less ceremony.
Safe navigation ?.
def order = [id: 'PO-1', customer: [name: 'Acme', address: null]]
order.customer?.address?.city // null — no exception
order.shipTo?.name // null — the key doesn't exist
?. means “if the left side is null, stop and return null; otherwise carry on”. Chain it through any path that might be incomplete.
Elvis ?: for defaults
order.customer?.address?.city ?: 'no city'
def lines = order.lines ?: [] // never null: safe to iterate
Remember that ?: uses Groovy truth: an empty string or 0 also triggers the default. If 0 is valid, test for null explicitly: qty != null ? qty : 1.
Elvis assignment ?= (Groovy 3+)
def note = null
note ?= 'default note' // assign only if currently null/false
Safe indexing ?[]
def items = null
items?[0] // null
items?.size() ?: 0 // 0
Calls in a chain
def email = ' AP@ACME.TEST '
email?.trim()?.toLowerCase()
When a value is required, say so
Being null-safe doesn’t mean ignoring missing data that matters. When something is mandatory, fail fast with a clear message:
def requireId(Map o) {
Objects.requireNonNull(o?.id, 'order id is required')
}
In integrations, a clear “order id is required” in the error report is far more useful than a silently skipped record.
The full example
// null_safety.groovy — ?., ?:, safe indexing and avoiding NullPointerException.
def order = [id: 'PO-1', customer: [name: 'Acme', address: null]]
// Safe navigation: stop at null instead of throwing
println order.customer?.address?.city // null (no exception)
println order.shipTo?.name // null: missing key
// Elvis: a default when the value is null or "false"
println order.customer?.address?.city ?: 'no city'
// Elvis assignment (Groovy 3+)
def note = null
note ?= 'default note'
println note
// Safe index on lists and maps
def items = null
println items?[0] // null
println items?.size() ?: 0 // 0
// with a chain of calls
def email = ' AP@ACME.TEST '
println email?.trim()?.toLowerCase()
// Empty collections are safe to iterate
def lines = order.lines ?: []
lines.each { println it } // prints nothing
println "lines: ${lines.size()}"
// Asserting non-null with a helpful message
def requireId(Map o) {
Objects.requireNonNull(o?.id, 'order id is required')
}
try {
requireId([:])
} catch (NullPointerException e) {
println "Caught: ${e.message}"
}
Try it yourself
Write cityOf(Map payload) that returns payload.customer.addresses[0].city, or 'UNKNOWN' if any part is missing — including an empty addresses list.
Show a solution
def cityOf(Map payload) {
payload?.customer?.addresses?.getAt(0)?.city ?: 'UNKNOWN'
}
assert cityOf([customer: [addresses: [[city: 'Delhi']]]]) == 'Delhi'
assert cityOf([customer: [addresses: []]]) == 'UNKNOWN'
assert cityOf(null) == 'UNKNOWN'
getAt(0) on an empty list returns null instead of throwing, so ?. can carry on.
Key takeaways
?.stops at null instead of throwing;?[]does the same for indexes.?:supplies defaults;?=assigns a default only when needed.- Defaulting collections to
[]makes them always safe to iterate. - For required values, fail early with a clear message.