What you'll learn

  • Safe navigation ?. and safe indexing ?[]
  • Elvis ?: for defaults and ?= for default assignment
  • Safe patterns for collections
  • Failing clearly when a value is required

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.

Example files