Skip to main content
Version: 2.3

tSM SpEL List Operations

Complete documentation for list behavior, extensions, SpEL operators, and ArrayList methods.


Introduction

Lists in tSM SpEL are a core data structure used across:

  • service connector responses
  • spreadsheet rows
  • aggregation pipelines
  • transformation logic
  • workflow scripts
  • condition evaluation
  • UI expression logic

In tSM SpEL, a list literal:

#persons = [
{'name': 'Alice'},
{'name': 'Bob'}
]

is represented internally as java.util.ArrayList. This means:

  • Core SpEL operators apply (selection, projection, …)
  • tSM List Extension Methods apply (sum, groupBy, forEach, …)
  • Native Java ArrayList methods apply (add, clear, remove, …)

This guide covers all three layers, explains how they work, why they exist, and when to use which.


Variable Initialization in tSM SpEL

In most cases, the tSM SpEL evaluator automatically infers the correct data type of a variable based on the assigned value. For example:

#name = 'Alice'    // inferred as String
#age = 42 // inferred as Integer
#items = [1,2,3] // inferred as java.util.ArrayList

However, some operations require the variable to be initialized explicitly before use. This is especially important when you want to call a method on a list variable that has not been initialized yet.

Why initialization matters

If a variable is undefined (not assigned yet), SpEL treats it as null. Calling any list method (including native ArrayList methods such as .add(...)) on null results in an error:

Example — calling .add() on an uninitialized list

#items.add({'name':'Alice'})

Result

Error: Cannot invoke method 'add' on null object

This happens because #items has no value yet, so SpEL cannot determine what .add() should operate on.

Correct way: initialize the empty list first

To explicitly create an empty list, assign:

#items = []

This produces a new java.util.ArrayList.

Now list operations work as expected:

Example — properly initialized list

#items = []
#items.add({'name':'Alice'})
#items.add({'name':'Bob'})
#items

Result

[
{'name':'Alice'},
{'name':'Bob'}
]

When explicit initialization is required

You should explicitly initialize a list when:

✔ You want to build a list dynamically using .add() or .addAll()

#logs = []
#logs.add('START')
#logs.add('FINISH')

✔ You want to gradually construct a collection inside a forEach

#result = []
#numbers = [1,2,3]
#numbers.forEach(#n, #result.add(#n * 10))
#result

✔ You want to ensure a variable is a list, not accidentally null

Some connectors return null when no records are present; converting it to a list may be safer:

#rows = #rows == null ? [] : #rows

✔ You want to avoid type ambiguity

If you later do:

#items = []
#items.add(123)
#items.add({'x':1})

SpEL won't complain — but your workflow might. Initializing avoids “null-type guessing” problems.

When explicit initialization is NOT required

You do not need to initialize a list if:

  • you assign a list literal:

    #items = [ {'name':'Alice'} ]
  • the list comes from:

    • a connector response
    • a spreadsheet
    • a service call
    • a previous expression

    These are always valid lists.


Core SpEL List Operators

Core SpEL operators are processed by the Spring Expression Engine, not by tSM. They work on any list, including lists returned from connectors and spreadsheets.

These two operators are foundational:

  • Selection: list.?[ condition ]
  • Projection: list.![ expression ]

They behave functionally and never modify the list.

Selection Operator — .?[ condition ]

What it does

Filters the list by evaluating a condition for each element. Every element where the condition evaluates to true is included in the resulting list.

How it works internally

  • The engine iterates over the list.

  • For each element:

    • Sets #this to the current element.
    • Evaluates the condition.
  • Builds a new list with elements for which the condition was true.

Correct syntax

list.?[ <boolean expression using #this> ]

When to use it

  • Filtering rows returned from connectors.
  • Filtering children in hierarchical data.
  • Powerful alternative to complex filter(...) calls.

Example – Select active persons

#persons = [
{'name': 'Alice', 'active': true },
{'name': 'Bob', 'active': false},
{'name': 'Carol', 'active': true }
]
#persons.?[#this.active]

Result explanation:

  • #this.active is true for Alice and Carol.
  • A new list with only those two persons is returned.

Example – Combined conditions

#persons = [
{'name': 'Alice', 'age': 30, 'role': 'ADMIN'},
{'name': 'Bob', 'age': 40, 'role': 'USER'},
{'name': 'Carol', 'age': 35, 'role': 'ADMIN'}
]
#persons.?[
#this.role == 'ADMIN' and #this.age >= 35
]

Why this is correct

The selection operator allows any boolean expression, including:

  • comparisons
  • logical operators
  • nested access (#this.role, #this.address.city)
  • function calls (e.g. isAfter)
  • safety navigation (?.)

Example – Selecting based on nested lists

#companies = [
{
'name': 'A',
'departments': [
{'code': 'IT'}, {'code': 'HR'}
]
},
{
'name': 'B',
'departments': [
{'code': 'IT'}, {'code': 'SALES'}
]
}
]

#companies.?[
#this.departments.?[#this.code == 'IT'].size() > 0
]

Explanation

We filter companies by checking if their departments list contains at least one with code == 'IT'.

Projection Operator — .![ expression ]

What it does

Maps every element of a list into something else:

  • a single field
  • a computed value
  • a complex object/map

Syntax

list.![ <expression using #this> ]

When to use

  • Extracting fields (IDs, names, totals…)
  • Building new objects from list elements
  • Transformations before passing values into connectors
  • Preparing list-of-values for dropdowns

Example – Extract all names

#persons = [
{'name': 'Alice', 'age': 30},
{'name': 'Bob', 'age': 40},
{'name': 'Carol', 'age': 35}
]
#persons.![#this.name]

Explanation

Projection produces:

['Alice', 'Bob', 'Carol']

Example – Complex projection

#persons = [
{'name': 'Alice', 'role': 'ADMIN'},
{'name': 'Bob', 'role': 'USER'}
]
#persons.![{
'label': #this.name + ' (' + #this.role + ')',
'role': #this.role
}]

Explanation

Transforms each input map into a new map.

Example – Selection + projection chain

This is extremely common in real workflows.

#persons = [
{'name': 'Alice', 'active': true},
{'name': 'Bob', 'active': false},
{'name': 'Carol', 'active': true}
]
#persons.?[#this.active].![#this.name]

Explanation

  • Select only active persons
  • Extract their names

tSM SpEL List Extension Methods

These are custom, domain-tailored functions provided by SpelContextContributorListSupport.

They provide:

  • aggregations (sum, average)
  • grouping (groupBy, toMap)
  • transformations (flatMap, mapNotNull)
  • searching (find, findIndex)
  • logic (any, all, none)
  • chunking (chunk, sliding)
  • iteration (forEach)

These do not mutate the original list unless specified.

Aggregations

sum() – simple numeric sum

Explanation

  • Converts values to Long.
  • Only for lists already containing numeric or numeric-string values.
  • Throws an error for non-numeric strings.

Example

#amounts = ['100','200','300']
#amounts.sum()

sum(mapperExpr) / sum(#var, mapperExpr)

Explanation

Mapped version:

  • Evaluates expression per element
  • Extracts numeric values
  • Returns Double
  • #index is available
  • Useful for summing object fields

Example

#orders = [
{'id':1,'total':100},
{'id':2,'total':200},
{'id':3,'total':300}
]
#orders.sum(#this.total)

average(mapperExpr)

Explanation

  • Uses same mapping rules as sum
  • Empty list → returns 0.0

Example

#orders = [
{'total':100},
{'total':50},
{'total':150}
]
#orders.average(#this.total)

Grouping and Mapping

groupBy(mapperExpr)

Explanation

  • Groups list elements into a map, effectively key -> List(elements with that key)
  • Key can be: field, expression, computed key, composite key

Example

#persons = [
{'name':'Alice','role':'ADMIN'},
{'name':'Bob', 'role':'USER'},
{'name':'Carol','role':'ADMIN'}
]
#persons.groupBy(#this.role)

toMap(keyExpr, valueExpr)

Explanation

  • Builds a map from a list
  • Key collisions overwrite the previous value
  • Very useful for lookup dictionaries

Example

#persons = [
{'id':1,'name':'Alice'},
{'id':2,'name':'Bob'}
]
#persons.toMap(#this.id, #this.name)

flatMap(mapperExpr)

Explanation

  • If mapper returns a list → its contents are appended
  • If returns a value → appended directly
  • If returns null → ignored
  • Flattens one level of nesting

Example

#parents = [
{'name':'P1', 'children':['C1','C2']},
{'name':'P2', 'children':['C3']}
]
#parents.flatMap(#this.children)

mapNotNull(mapperExpr)

Explanation

  • Maps elements
  • Drops all null results
  • Useful for safely extracting optional values

Example

#rows = [
{'value':'A'},
{'value':null},
{'value':'B'}
]
#rows.mapNotNull(#this.value)

Logic & Searching

any(condition)

True if at least one element matches

#orders = [
{'total':50},
{'total':150}
]
#orders.any(#this.total > 100)

all(condition)

True if all match

#persons = [{'valid':true},{'valid':true}]
#persons.all(#this.valid)

none(condition)

True if none match

#persons = [{'disabled':false},{'disabled':false}]
#persons.none(#this.disabled)

find(condition)

Returns the first matching element

#persons = [
{'name':'Alice','age':30},
{'name':'Bob', 'age':40}
]
#persons.find(#this.age > 35)

findIndex(condition)

Returns index of first match

#persons = [
{'age':30},
{'age':40}
]
#persons.findIndex(#this.age > 35)

Chunking & Sliding

chunk(size)

Splits into sublists of fixed size.

#persons = [{'name':'A'},{'name':'B'},{'name':'C'},{'name':'D'}]
#persons.chunk(2)

sliding(window, step)

Creates sliding windows.

#persons = [{'name':'A'},{'name':'B'},{'name':'C'},{'name':'D'},{'name':'E'}]
#persons.sliding(3, 2)

forEach(#var, expr...)

Explanation

  • First argument must be a variable reference.
  • All following expressions run for each element.
  • Result of the last expression builds the output list.
  • Supports #index.

Example

#persons = [
{'name':'Alice','age':30},
{'name':'Bob', 'age':40}
]
#persons.forEach(
#p,
{'index': #index, 'label': #p.name + ' (' + #p.age + ')'}
)

Native Java ArrayList Methods

In tSM SpEL, list literals like:

#list = [1, 2, 3]

are implemented as java.util.ArrayList. That means you can call many native Java methods directly on list variables.

However:

  • Some methods are very useful in SpEL (e.g. add, remove, contains, …).
  • Others are low-level JVM details (e.g. ensureCapacity, trimToSize) with no visible effect in SpEL.
  • Some require Java 8 functional interfaces (Predicate, UnaryOperator, …) and are not practical to use from SpEL expressions.

This section documents recommended native methods with examples.

Important note: initialize the list first

Native methods operate on the actual list instance. If the variable is not initialized, it is null and you will get an error:

#list.add(1)   // ERROR: cannot call add on null

Always initialize an empty list before first use:

#list = []     // creates new java.util.ArrayList

After that, all examples below will work as expected.

Methods

These methods are simple, predictable and genuinely useful in tSM SpEL.

size() – number of elements

Returns the number of elements in the list.

#list = [10, 20, 30]
#list.size()

Result: 3

isEmpty() – check if list is empty

Returns true if the list contains no elements.

#list = []
#list.isEmpty()

Result: true

get(index) – get element by index

Returns the element at the given index (0-based). Throws an error if index is out of range.

#list = [
{'name':'Alice'},
{'name':'Bob'},
{'name':'Carol'}
]
#list.get(1)

Result:

{'name':'Bob'}

add(element) – append element to the end

Adds a new element at the end of the list.

Typical use: dynamically building a list in a script.

#list = []
#list.add({'name':'Alice'})
#list.add({'name':'Bob'})
#list

Result:

[
{'name':'Alice'},
{'name':'Bob'}
]

add(index, element) – insert at position

Inserts an element at a specific position and shifts following elements to the right.

#list = [
{'name':'Alice'},
{'name':'Carol'}
]
#list.add(1, {'name':'Bob'})
#list

Result:

[
{'name':'Alice'},
{'name':'Bob'},
{'name':'Carol'}
]

addAll(otherList) – append multiple elements

Appends all elements from another collection to the end of the list.

#admins = [
{'name':'Alice', 'role':'ADMIN'}
]
#users = [
{'name':'Bob', 'role':'USER'},
{'name':'Carol', 'role':'USER'}
]

#admins.addAll(#users)
#admins

Result:

[
{'name':'Alice', 'role':'ADMIN'},
{'name':'Bob', 'role':'USER'},
{'name':'Carol', 'role':'USER'}
]

remove(index) – remove by index

Removes the element at the specified index and returns it. In the console you usually ignore the return value and inspect the modified list.

#list = [
{'name':'Alice'},
{'name':'Bob'},
{'name':'Carol'}
]
#list.remove(1)
#list

Result:

[
{'name':'Alice'},
{'name':'Carol'}
]

remove(element) – remove first occurrence

Removes the first occurrence of the given value from the list.

This is most reliable with simple values (numbers, strings, enums).

#roles = ['ADMIN','USER','GUEST','USER']
#roles.remove('USER')
#roles

Result:

['ADMIN','GUEST','USER']

For map/object values, equality depends on how the underlying type implements equals. For most SpEL literals, it is safer to use tSM helpers like find / none / selection (.?[...]) instead.

clear() – remove all elements

Removes all elements from the list, making it empty.

#list = [
{'name':'Alice'},
{'name':'Bob'}
]
#list.clear()
#list

Result:

[]

contains(element) – check if value is present

Returns true if the list contains the value.

Best for simple types (strings, numbers, enums):

#roles = ['ADMIN','USER','GUEST']
#roles.contains('USER')

Result: true

For complex maps, prefer logical methods:

#persons = [
{'name':'Alice'},
{'name':'Bob'}
]
#persons.any(#this.name == 'Alice') // recommended

indexOf(element) – first index of value

Returns the index of the first occurrence of element or -1 if not found.

#list = [10, 20, 30, 20]
#list.indexOf(20)

Result: 1

lastIndexOf(element) – last index of value

Returns the index of the last occurrence of element or -1 if not found.

#list = [10, 20, 30, 20]
#list.lastIndexOf(20)

Result: 3

subList(fromIndex, toIndex) – view of a slice

Returns a view of the list between fromIndex (inclusive) and toIndex (exclusive).

#list = [
{'id':1},
{'id':2},
{'id':3},
{'id':4}
]
#list.subList(1, 3)

Result:

[
{'id':2},
{'id':3}
]

Note: this is a view backed by the original ArrayList. Changes in the base list can affect the sublist and vice versa.

clone() and toArray() – advanced / niche usage

These methods work but are typically used only in advanced scenarios.

clone()

Creates a shallow copy of the list (elements themselves are not cloned).

#list = [1, 2, 3]
#copy = #list.clone()
#copy

Result: a new list [1, 2, 3].

toArray()

Creates a Java Object[] array from the list. Sometimes useful when passing data into an API that expects an array, not a list.

#list = [10, 20, 30]
#arr = #list.toArray()
#arr

Output format depends on how your tools render Java arrays.


For list operations in tSM SpEL, think in layers:

  1. Core SpEL operators list.?[...] for filtering, list.![...] for mapping → functional, safe, no side effects.

  2. tSM list extensions sum, average, groupBy, toMap, flatMap, mapNotNull, any, all, none, find, findIndex, chunk, sliding, forEach, filter → high-level helpers for real business logic.

  3. Native ArrayList methods (this chapter) add, addAll, remove, clear, contains, indexOf, lastIndexOf, subList, size, … → low-level mutable tools when you need to build or tweak a list manually.