IM 8.1.0 — Groovy 4 to 5 Migration

Groovy 4.0.30 → 5.0.8 · sandbox fork im-groovy-sandbox 1.19 → 2.0

This is the single highest-risk part of the IM 8.1.0 upgrade for customer integrations, because Groovy is not a build-time detail here — it is the language your mappers, filters, converters and route expressions are written in, and IM compiles and runs those scripts at runtime.

Why this deserves a careful read. Most Groovy 5 changes are not compile errors. A script that deployed fine on IM 8.0 will still deploy on 8.1.0 and then produce a different value. The sections tagged SILENT below are the ones to search your repository for.


How to use this page

  1. Work through §1 Silent behaviour changes first — these cannot be caught by deploying, only by reading code or by output diffing.

  2. Then §2 Changes that fail loudly — these surface as a failed deployment or a runtime exception, so a deploy of every script is an adequate test.

  3. Then §3 Sandbox changes — IM-specific, affects which classes your scripts may touch at all.

  4. §4 Search patterns gives concrete greps.

Legend: SILENT = same code, different result, no error · DEPLOY-FAIL = script no longer compiles · RUNTIME = throws when executed.


1. Silent behaviour changes

1.1 File / Path truthiness now means "exists" — SILENT

Groovy 5 changed the Groovy-truth of java.io.File and java.nio.file.Path. In Groovy 4 a non-null File object was truthy. In Groovy 5 the file is truthy only if it exists on disk.

Groovy
// BEFORE (Groovy 4): true whenever the object is non-null
def f = new File('/data/inbox/orders.csv')
if (f) {
    // reached even when the file does not exist yet
}

// AFTER (Groovy 5): false when the file does not exist
if (f) {
    // now skipped for a not-yet-created file
}

Impact in IM. Scripts that build an output File and test it before writing, or that null-check a File parameter with if (file), change branch. Nothing throws — the branch is just not taken.

Fix. Be explicit about intent:

Groovy
if (f != null)      // "I have a file object"
if (f.exists())     // "the file is on disk"

Groovy 5.0.7 added a system property to restore the old behaviour, but relying on it only defers the problem — prefer making the test explicit.


1.2 % operator dispatches to remainder() instead of mod()SILENT

For custom types that define their own mod() method, % no longer routes to it; Groovy 5 looks for remainder() first. The two differ for negative operands (mod is floor-based, remainder is truncation-based).

Groovy
// A custom type with a mod() implementation
class Cycle {
    int value
    Cycle mod(Cycle other)       { /* floor semantics */ }
}

def r = a % b   // Groovy 4 -> a.mod(b)
                // Groovy 5 -> a.remainder(b) if present, otherwise falls back

Impact in IM. Only affects scripts that define custom numeric-ish types, or that use % on BigDecimal/BigInteger values where the sign can be negative. Plain int % int is unchanged.

Fix. Call the method explicitly (a.mod(b)) where floor semantics are required.


1.3 Property access on Map-implementing classes now prefers the real field — SILENT

(Apache tickets GROOVY-6144 / GROOVY-5001, fixed in 5.0.0-alpha-1)

For a class that implements Map (or extends HashMap/Properties), dot-property access used to be able to resolve to a map entry and shadow a real declared field. Groovy 5 reorders resolution so a declared field/getter wins; subscript access still always reads the map entry.

Groovy
class Payload extends HashMap {
    String name = 'field-value'
}
def p = new Payload()
p.put('name', 'map-value')

p.name        // Groovy 4: could resolve to 'map-value' (and did so inconsistently
              //           inside closures); Groovy 5: 'field-value'
p['name']     // both versions: 'map-value'   <- unambiguous, prefer this
p.getName()   // both versions: 'field-value' <- unambiguous, prefer this

Also fixed in the same change: map.class now returns the Class object instead of null, and map['metaClass'] = x no longer throws ClassCastException.

Impact in IM. Mapper and converter scripts that extend HashMap to build a record, or that treat a map-like DTO with both entries and fields, can now read a different value. Nothing throws.

Fix. Use obj['key'] when you mean the map entry and obj.getX() when you mean the property.


1.4 @CompileStatic overload selection realigned with dynamic dispatch — SILENT

(GROOVY-8788, fixed in 5.0.0-alpha-1; the Groovy authors label this a breaking change)

Under @CompileStatic, Groovy 4's static type checker could pick a different overload than the dynamic runtime would for the same call — notably for extension methods with overlapping self/parameter types. Groovy 5 aligns the static checker with runtime dispatch ("prefer closer parameter matching").

Impact in IM. Only scripts annotated @CompileStatic that call overloaded methods where more than one candidate applies. The call still compiles; a different method runs.

Fix. Remove the ambiguity — add an explicit cast at the call site, or avoid overlapping overloads.


1.5 Nest-based access replaces synthetic accessor methods — SILENT (bytecode)

(GROOVY-10687, fixed in 5.0.0-alpha-11)

Groovy 4 generated synthetic access$000-style bridge methods so inner classes and closures could reach private members of their enclosing class. Groovy 5 uses JEP 181 nestmates (NestHost/NestMembers attributes) and no longer emits those bridge methods.

Impact in IM. Invisible for ordinary scripts. It matters if a script (or a helper class deployed as an IM class) uses reflection to enumerate methods and expects the old synthetic accessors, or asserts on a class's exact method inventory.

This is also the area that historically interacted badly with the sandbox — see §3.2.


1.6 Annotation retention changed to SOURCE for AST transforms — SILENT (reflection)

@ToString, @EqualsAndHashCode, @Immutable, @Canonical, @AutoClone, @Sortable and related AST-transform annotations moved from RUNTIME/CLASS retention to SOURCE. The generated code is unchanged; the annotation is simply no longer present on the compiled class.

Groovy
@ToString
class Order { String id }

// Groovy 4: Order.class.annotations contains ToString
// Groovy 5: it does not — the annotation is gone after compilation

Impact in IM. Only scripts that reflect over their own annotations. toString() itself still works exactly as before.


1.7 Set minus semantics and other collection tweaks — SILENT

Set minus behaviour changed; where the previous element-removal semantics are required, use removeAll. chop() now stops at exhaustion instead of padding with empty lists, and its Iterator variant returns Iterator<List>. findIndexValues() returns an Iterator rather than a List.

Groovy
def idx = list.findIndexValues { it > 10 }
idx.size()              // Groovy 4: List.size()
                        // Groovy 5: Iterator has no size() -> MissingMethodException
def idxList = list.findIndexValues { it > 10 }.toList()   // portable

findIndexValues is the one most likely to throw rather than silently differ, because Iterator lacks the List methods scripts typically call next.


2. Changes that fail loudly

2.1 Duplicate imports are now a compile error — DEPLOY-FAIL

Groovy 4 silently accepted a repeated import (last one won). Groovy 5 rejects it.

Groovy
import java.time.LocalDate
import java.time.LocalDate   // Groovy 5: compilation fails

Impact in IM. Generated or copy-pasted mapper scripts sometimes accumulate duplicate imports. These fail at deploy time, so deploying every script surfaces them all.


2.2 abstract / final on enums is now a compile error — DEPLOY-FAIL

Previously ignored, now rejected. Remove the modifier.


2.3 java.time is auto-imported — DEPLOY-FAIL (name clash)

Groovy 5 adds java.time.* to the default imports. A customer class in the default package named Duration, Period, Instant, Clock etc. now collides.

Fix. Rename the class, or fully-qualify the reference.


2.4 Stricter generics checking under @CompileStaticDEPLOY-FAIL

(GROOVY-9074, fixed in 5.0.0-alpha-12)

Groovy 5 implements Java-style wildcard capture in the static type checker. Code that was unsafe but previously compiled is now rejected, matching javac:

Groovy
@CompileStatic
class Holder {
    static Collection<?> c = new ArrayList<String>()
    static void main(String[] args) {
        c.add(new Object())   // Groovy 4: compiled, silently corrupted the list
                              // Groovy 5: compile error (capture conversion)
    }
}

Fix. Use the concrete generic type, or a properly bounded wildcard (<? extends X> / <? super X>).


2.5 Anonymous inner classes are package-private — RUNTIME (reflection only)

(GROOVY-11481, fixed in 5.0.0-alpha-11)

Groovy 5 emits anonymous inner classes with default (package-private) access, matching javac; Groovy 4 marked them public. Cross-package reflective access to such a class can now throw IllegalAccessException. Normal use through the implemented interface is unaffected.

Fix. Use a named, explicitly public class where external reflective access is required.


2.6 Scripts with only a static main no longer extend ScriptRUNTIME

Such a script class no longer inherits from groovy.lang.Script, so the binding is not reachable via this. IM mapper/filter expressions are not written this way, but standalone helper scripts might be.


2.7 Removed / changed internals

  • GroovyClassLoader.sourceCache changed type to the FlexibleEvictableCache interface.

  • The $getLookup JPMS workaround hook was removed.

  • The spread-dot operator's null handling changed in an edge case.

These affect code that reaches into Groovy internals — rare in customer scripts, relevant for deployed helper classes that do clever things.


3. Sandbox changes (IM-specific)

IM executes customer Groovy inside a sandbox (see groovy-sandbox.md) built on a Pricefx fork of groovy-sandbox. IM 8.1.0 moves that fork from 1.19 → 2.0, which is the Groovy 5 line.

3.1 The Jackson classes on the whitelist changed package — DEPLOY-FAIL

This is a direct consequence of the Jackson 2 → 3 migration (see jackson-2-to-3-migration.md). The sandbox whitelist previously allowed:

com.fasterxml.jackson.databind.JsonNode
com.fasterxml.jackson.core.JsonParser
com.fasterxml.jackson.core.JsonToken

IM 8.1.0 allows the Jackson 3 equivalents instead:

tools.jackson.databind.JsonNode
tools.jackson.core.JsonParser
tools.jackson.core.JsonToken
Groovy
// BEFORE
import com.fasterxml.jackson.databind.JsonNode

// AFTER
import tools.jackson.databind.JsonNode

A script still importing the com.fasterxml classes is rejected by the sandbox — the class is no longer on the allow-list, so this fails rather than silently working.

3.2 Anonymous inner classes and closures capturing variables

groovy-sandbox.md already documents a restriction here: anonymous inner classes and closures that capture external variables were not permitted under the sandbox on OpenJDK 17/21 with Groovy 4.0.15. Groovy 5's move to nestmates (§1.5) changes exactly the bytecode mechanism this restriction relates to, and the sandbox fork's SandboxTransformer was updated for Groovy 5.

Recommendation. If your scripts use closures that capture external variables — most do — include a representative sample in your pre-production verification. This is the area where the sandbox and the compiler interact most tightly, and it is worth exercising rather than assuming.

3.3 Sandbox fix carried in 2.0

The 2.0 fork also carries PFIMCORE-3104, which fixes super method calls being erased by the sandbox transformer. Scripts that call super.someMethod() behave correctly on 8.1.0 where they may have misbehaved before.


4. Search patterns for your repository

Run these against your customer routes, mappers, filters and deployed Groovy classes.

What to find

Pattern

Why

Jackson imports

com\.fasterxml\.jackson

§3.1 — sandbox now allows only tools.jackson

File truthiness

if\s*\(\s*\w*[Ff]ile\w*\s*\)

§1.1 — meaning changed to "exists"

findIndexValues

findIndexValues

§1.7 — returns Iterator now

chop(

\.chop\(

§1.7 — padding behaviour changed

Custom mod

`def mod\(

Object mod\(`

§1.2 — % no longer routes here

@CompileStatic

@CompileStatic

§1.4, §2.4 — overload selection + generics

Map-extending classes

`extends HashMap

implements Map`

§1.3 — property resolution reordered

Duplicate imports

(deploy and read the error)

§2.1 — fails at deploy

Reflection on annotations

`\.annotations

getAnnotation`

§1.6 — AST annotations are SOURCE now

super. calls

super\.

§3.3 — previously erased by the sandbox


5. What IM does for you

You do not need to act on these — they are handled inside IM:

  • The sandbox fork, its transformer and the call-site interceptors were rebuilt for Groovy 5.

  • The whitelist itself was migrated (only the Jackson entries changed package).

  • IM's own Groovy code and its ~4 000 tests run on Groovy 5.0.8.

  • Spock moved to 2.4-groovy-5.0 — relevant only if you build against IM's test artifacts.


6. Sources

  • Apache Groovy 5.0 release notes

  • Apache JIRA: GROOVY-6144, GROOVY-5001, GROOVY-8788, GROOVY-9074, GROOVY-11481, GROOVY-10687

  • IM: groovy-sandbox.md, classes-groovy.md