In Gatling's Expression Language, what do `#{ids(0)}`, `#{order.total}` and `#{ids.size()}` each read, and what may appear inside the index parentheses?
answer
- Attributes are not always strings
- Index, key, size and random
- Negative index counts from the end
- An attribute name can be the index
- Nested placeholders fail at compile time
basics
~20 s#{ids(0)} takes an element by index, #{order.total} takes a key or field, and #{ids.size()} gives a collection's length. The index may be a literal, a negative offset from the end, or the name of another Session attribute.
solid answer
~40 sA Gatling Session attribute does not have to be a String. `#{ids(0)}` indexes into an array, a `java.util.List`, a Scala `Seq` or a `Product`; a negative index counts backward from the end, so `#{ids(-1)}` is the last element. `#{order.total}` reads a key or field out of a `java.util.Map`, a POJO, a Java record, a Scala `Map` or a case class. `#{ids.size()}` returns the length of a collection and `#{ids.random()}` picks one element at random. The forms chain — `#{orders.items.random()}` and `#{matrix(0)(0)}` are both legal. Inside the parentheses you may put an integer literal or the **name of another Session attribute** holding the index; you may not nest a placeholder, and `#{ids(#{i})}` is rejected when the expression is compiled.
code
java · 5 lines// ids holds a java.util.List, order holds a Map, a record or a POJO
exec(http("Reorder")
.get("/accounts/#{accountId}/orders/#{ids(-1)}")
.queryParam("total", "#{order.total}")
.queryParam("itemCount", "#{ids.size()}"));go deeper
Be ready to recognise the index, key and size forms in someone else's simulation and say what each one is reading out of the Session.
Be ready to explain which value types each form accepts, why a negative index is legal, and that the parentheses may hold another attribute's name.
Be ready to debug a chained expression from the build-failure message alone, and to say why a long chain is a liability compared with a function.
Be ready to set a house rule on how much logic belongs in an expression string versus in typed code, and to justify it by what each one fails at and when.
Gatling Session attributes are typed values, not strings. A feeder can put a list in one, a check can save a parsed object into another, and a function can store anything at all. The Expression Language therefore offers more than a bare name lookup: it can index, key, count and sample, and those forms compose. ## The four access forms | form | reads | supported value types | |---|---|---| | `#{ids}` | the attribute itself | any | | `#{ids(0)}` | an element by position | array, `java.util.List`, Scala `Seq`, `Product` | | `#{order.total}` | a value by key or field name | `java.util.Map`, POJO, Java record, Scala `Map`, case class | | `#{ids.size()}` | the number of elements | array, `java.util.Collection`, `java.util.Map`, Scala `Iterable`, `Product` | One more attribute form exists: `#{body.htmlUnescape()}` decodes HTML entities in a String attribute, so a value captured out of a rendered page can be sent back in its decoded form. Alongside `size()` sits `#{ids.random()}`, which returns one element chosen uniformly at random on every evaluation. It accepts arrays, `java.util.List`, Scala `Seq` and `Product` — a narrower set than `size()`, which also counts a `java.util.Map` and any Scala `Iterable`. A tuple form also exists for Scala tuples and other `Product` values: `#{pair._1}` reaches the **first** component and `#{pair._2}` the second, using the same one-based underscore spelling Scala itself uses. ## Index access has three legal contents This is the part most people have not read. The characters between the parentheses can be: 1. **A non-negative integer literal** — `#{ids(0)}` is the first element. 2. **A negative integer literal** — `#{ids(-1)}` is the last, `#{ids(-2)}` the one before it. Gatling adds the collection's length to the negative value to get the real position. 3. **The name of another Session attribute** — `#{ids(cursor)}` looks up the attribute `cursor`, validates it as an `Int`, and uses that as the index. This is how you index dynamically without leaving EL. What you cannot do is nest one placeholder inside another. `#{ids(#{cursor})}` and `#{#{name}}` are both rejected, and the rejection happens when the expression is **compiled**, not when a request runs. That matters for the failure you see: a malformed expression raises an `ElParserException` while the simulation is being built, so the run never starts. The same is true of an unclosed `#{foo` and of an empty `#{}`. ## The character set a name may use An attribute name in an expression is read by one regular expression, and it excludes six characters: `.`, `#`, `{`, `}`, `(` and `)`. The dot is the consequential one, because it is how key access is spelled. An attribute literally named `user.name` — a shape a data file can easily produce — cannot be addressed at all: `#{user.name}` is parsed as *key `name` of attribute `user`*, and it fails because no attribute called `user` exists. There is no quoting or escaping that changes this. The fix is upstream, in whatever named the attribute: rename it to something dot-free, or read it in a function, where the key is an ordinary String argument and the parser is not involved. ## Composition The access forms chain left to right, and each link is applied to the result of the one before it: * `#{orders.items.random()}` — key `orders`, then key `items`, then a random element of that list. * `#{matrix(0)(0)}` — first element of the first inner collection. * `#{ids.size()}` used as a value in its own right, for instance as a query parameter. Because each link can fail independently, the error message names the path that failed rather than just the attribute: an out-of-range index reports the sequence's name and the index it was given, and a missing key reports the map's name and the key. ## What a failure does Every one of these forms produces a failure rather than a `null` when it cannot resolve — a missing attribute, an index past the end, a key the map does not hold, or a `size()` call on a value that is not a collection. When that expression is part of a request, Gatling cannot build the request at all: it logs `Failed to build request <name>: <reason>`, records one entry in the run's Errors table — no request record, so no request count, no KO and no response time — and lets the virtual user carry on with its Session marked as failed. Reading that message is usually the fastest way to find which link of a chained expression broke. ## Using it ```java // ids holds a java.util.List, order holds a Map, a record or a POJO exec(http("Reorder") .get("/accounts/#{accountId}/orders/#{ids(-1)}") .queryParam("total", "#{order.total}") .queryParam("itemCount", "#{ids.size()}")); ``` Two habits keep these expressions maintainable. **Keep the chains short** — a four-link expression is a string with no compile-time checking behind it, and the moment it needs a conditional it should be a function instead. And **prefer the explicit index to `random()`** when a test needs to be reproducible, because `random()` draws afresh on every evaluation, so two occurrences of the same expression in one request will not agree.
- What does `#{ids(cursor)}` do when `cursor` is a Session attribute rather than a number?Gatling reads the `cursor` attribute, validates it as an `Int`, and uses that value as the index into `ids`. It is the supported way to index dynamically. If `cursor` is missing or is not convertible to an `Int`, the expression fails and the request cannot be built.
- Why does `#{ids(#{cursor})}` fail, and when does it fail?Nesting one placeholder inside another is not part of the grammar, so the parser rejects it with an `ElParserException`. It fails when the expression is compiled — while the simulation is being built — rather than during the run, so the whole simulation stops before any user starts. Use the bare attribute name inside the parentheses instead.
saying these in an interview costs you the question
- Assuming a Session attribute can only ever hold a String
- Writing a nested placeholder to build an index or an attribute name
- Expecting an out-of-range index to resolve to null instead of failing
- Thinking random() picks once and reuses that element for the request
- Believing size() works on any value rather than on collections