
# Hashmaps and arrays

> Build and read hashmaps and arrays in Velocity — literal syntax, adding and retrieving entries, iterating them, and the type pitfalls that catch people out.

URL: https://www.hannonhill.com/cascadecms/latest/articles/developing-in-cascade/script-formats/working-with-hashmaps-arrays.html
category: Developing in Cascade
article-type: how-to
content-kind: article
release-availability: all
tags: velocity
updated: 2026-10-06

## Overview

Velocity provides two mutable collection types: **lists** (ordered sequences) and **hashmaps** (key-value pairs). In Cascade CMS, these are the primary tools for aggregating data across query results, grouping pages by category, building JSON output, and collecting filtered results—any scenario where you need to accumulate or organize more than one value.

- Create an empty list with bracket syntax or a pre-populated literal list.
- Append items with `.add()`, access by position with `.get(0)` or `$list[0]`.
- Check size with `.size()` and iterate with `#foreach`.
- Create a map with literal key-value pairs or start empty and populate it in a loop.
- Add, update, and read values by key using `.put()` and `.get()`.
- Iterate all entries with `#foreach ($entry in $map.entrySet())`.
- Check for key existence with `.containsKey()`.
- Nest lists inside maps and maps inside lists for grouped data structures.

## Lists

Lists are ordered, zero-indexed collections. Use them when you need to accumulate items, iterate a sequence, or collect filtered results from a query.

### Creating a list

You can create a list with literal values or start empty.

```velocity
## Empty list - growable, you can .add() to this
#set ($pages = [])

## Literal list with values
#set ($colors = ["red", "green", "blue"])

## Read from a literal list
$colors.get(0)    ## red
$colors[1]        ## green
$colors.size()    ## 3
```

### Adding items

`.add("value")` appends to the end of a list and returns `true`. Suppress the return value with `$void`.

```velocity
#set ($titles = [])

#set ($void = $titles.add("Spring Newsletter"))
#set ($void = $titles.add("Fall Events"))
#set ($void = $titles.add("Annual Report"))

$titles.size()    ## 3
```

> **Tip**
>
> Methods like `.add()` and `.put()` return a value—`true` for `.add()`, or the previous value for `.put()`. If you call them on their own, Velocity prints that return value straight into your page output. Wrapping the call in `#set ($void = ...)` captures the return into a throwaway variable so nothing is rendered. The variable name `$void` is just a convention—any name works—but it signals that you are intentionally discarding the result.

Collect titles from query results that match a condition:

```velocity
#set ($featuredTitles = [])

#set ($results = $_.query().byContentType("News").preloadDynamicMetadata().maxResults(100).execute())

#foreach ($page in $results)
    #if ($page.metadata.getDynamicField("featured").value == "Yes")
        #set ($void = $featuredTitles.add($page.metadata.title))
    #end
#end

## featuredTitles now contains only the featured page titles
```

### Accessing by index

Two equivalent syntaxes. Use `.get()` when the index is held in a variable.

**Syntax options for reading list values.**

| Syntax | Example | Notes |
| --- | --- | --- |
| `.get(n)` | `$list.get(0)` | Works with variable index |
| Bracket notation | `$list[0]` | Concise shorthand |

```velocity
#set ($tags = [])
#set ($void = $tags.add("velocity"))
#set ($void = $tags.add("cascade"))
#set ($void = $tags.add("formats"))

$tags.get(0)     ## velocity
$tags[1]         ## cascade
$tags.size()     ## 3

## Last item
#set ($last = $tags.size() - 1)
$tags.get($last) ## formats
```

### Iterating a list

Standard `#foreach` works as expected. Use `$foreach.count` for 1-based numbering, `$foreach.index` for 0-based, and `$foreach.hasNext` for comma separation or last-item logic.

```velocity
#set ($sections = [])
#set ($void = $sections.add("Introduction"))
#set ($void = $sections.add("Methods"))
#set ($void = $sections.add("Results"))

<ol>
#foreach ($section in $sections)
    <li>$section</li>
#end
</ol>
```

Comma-separated output:

```velocity
#set ($tags = ["velocity", "cascade", "formats"])
#foreach ($tag in $tags)$tag#if ($foreach.hasNext), #end
#end
## Output: velocity, cascade, formats
```

Build a filtered navigation list from `$_.query()` results:

```velocity
#set ($navPages = [])

#set ($results = $_.query().byContentType("e").bySiteName($currentPage.siteName).sortBy("name").sortDirection("asc").maxResults(50).execute())

#foreach ($page in $results)
    #if ($page.metadata.getDynamicField("show-in-nav").value == "Yes")
        #set ($void = $navPages.add($page))
    #end
#end

<ul>
#foreach ($page in $navPages)
    <li><a href="$page.link">$page.metadata.displayName</a></li>
#end
</ul>
```

## Hashmaps

A hashmap stores key-value pairs. Velocity uses Java’s `LinkedHashMap` under the hood, so insertion order is preserved when you iterate.

### Creating a hashmap

You can create a map inline with values or start empty and populate it later.

```velocity
## Empty map
#set ($map = {})

## Pre-populated map
#set ($labels = {
    "news":   "News",
    "events": "Events",
    "about":  "About"
})

## Map with numeric values
#set ($counts = {
    "published": 0,
    "draft":     0,
    "archived":  0
})
```

A common use case is a lookup table that maps machine names to human-readable labels:

```velocity
## Map content type names to display labels
#set ($typeLabels = {
    "news-article":  "News",
    "event-page":    "Events",
    "staff-bio":     "People",
    "press-release": "Press"
})

## Use the map inside a loop
#foreach ($page in $results)
    #set ($label = $typeLabels.get($page.contentTypeName))
    <span class="badge">$!label</span>
#end
```

> **Tip**
>
> Insertion order is preserved, so iterating a map you defined inline produces entries in the order you wrote them—useful for ordered navigation or display.

### Adding and updating entries

`.put("key", "value")` adds a new entry or overwrites an existing one. Because `.put()` returns the previous value (or null), wrapping it in `#set ($void = ...)` suppresses that output—this is the standard Cascade pattern.

```velocity
#set ($map = {})

## Add a new entry
#set ($void = $map.put("theme", "blue"))

## Overwrite an existing entry
#set ($void = $map.put("theme", "green"))

## Variable as the key
#set ($key = "layout")
#set ($void = $map.put($key, "two-column"))
```

Accumulate page counts by section while walking query results:

```velocity
#set ($sectionCounts = {})

#set ($results = $_.query().byContentType("Article").preloadDynamicMetadata().maxResults(-1).execute())

#foreach ($page in $results)
    #set ($section = $page.metadata.getDynamicField("section").value)

    ## Get existing count, default to 0
    #set ($current = $sectionCounts.get($section))
    #if (!$current)
        #set ($current = 0)
    #end

    ## Compute the new value first, then put
    #set ($n = $current + 1)
    #set ($void = $sectionCounts.put($section, $n))
#end
```

> **Note**
>
> Arithmetic only works inside `#set`, not inside method arguments. Always compute the new value first with `#set ($n = $old + 1)`, then call `.put()` with `$n`.

### Reading values

Three syntaxes exist for reading a value from a map. Use `.get()` when the key is held in a variable.

**Syntax options for reading map values.**

| Syntax | Example | When to use |
| --- | --- | --- |
| `.get("key")` | `$map.get("theme")` | Always works; required when key is a variable |
| Bracket notation | `$map["theme"]` | Concise for literal string keys |
| Dot notation | `$map.theme` | Concise, but fails for keys with hyphens or spaces |

```velocity
#set ($config = {
    "color":    "blue",
    "columns":  "3",
    "show-nav": "true"
})

## All three work for simple keys
$config.get("color")      ## blue
$config["color"]          ## blue
$config.color             ## blue

## Required when the key is in a variable
#set ($field = "columns")
$config.get($field)       ## 3

## Dot notation fails for hyphenated keys
## $config.show-nav       ## interpreted as subtraction - wrong
$config.get("show-nav")   ## true
$config["show-nav"]       ## true
```

> **Note**
>
> Keys with hyphens (common in Cascade field names like `"show-nav"`) cannot be accessed with dot notation. Use `.get()` or bracket notation instead.

### Iterating a hashmap

Use `#foreach ($entry in $map.entrySet())` to walk all entries. Each `$entry` has `.key` and `.value` properties. For keys only, use `.keySet()`; for values only, use `.values()`.

```velocity
#set ($nav = {
    "Home":   "/index",
    "News":   "/news/index",
    "Events": "/events/index"
})

<ul>
#foreach ($entry in $nav.entrySet())
    <li><a href="$entry.value">$entry.key</a></li>
#end
</ul>
```

To sort keys alphabetically, sort the key set first with `$_SortTool`:

```velocity
## Keys come back in insertion order by default
## Sort alphabetically with $_SortTool
#set ($sortedKeys = $_SortTool.sort($nav.keySet()))

<ul>
#foreach ($key in $sortedKeys)
    <li><a href="$nav[$key]">$key</a></li>
#end
</ul>
```

### Checking for keys

Use `.containsKey("key")` before reading when the key might not exist. Accessing a missing key returns `null`, which Velocity renders as the literal variable reference text unless you use `$!` to suppress it.

```velocity
#set ($labelMap = {
    "news":   "News",
    "events": "Events"
})

#set ($type = $currentPage.getStructuredDataNode("content-type").textValue)
#if ($labelMap.containsKey($type))
    <span class="label">$labelMap.get($type)</span>
#else
    <span class="label">$type</span>
#end
```

```velocity
## Safe read with $! (outputs nothing if key is missing)
$!config.get("theme")
```

## Quick reference

### List operations

**List operations at a glance.**

| Operation | Syntax | Notes |
| --- | --- | --- |
| Empty list | `#set ($list = [])` | Growable |
| Literal list | `#set ($list = ["a", "b"])` | Pre-populated |
| Add item | `#set ($void = $list.add("x"))` | Appends; returns `true` |
| Add all | `#set ($void = $list.addAll($other))` | Merges another list in |
| Read by index | `$list.get(0)` or `$list[0]` | Zero-based |
| Size | `$list.size()` |  |
| Iterate | `#foreach ($item in $list)` | `$foreach.hasNext`, `$foreach.count` |
| Contains check | `$list.contains("x")` | Returns boolean |
| Remove | `$list.remove($item)` |  |
| Sort | `$_SortTool.sort($list)` | Returns new sorted list |

### Hashmap operations

**Hashmap operations at a glance.**

| Operation | Syntax | Notes |
| --- | --- | --- |
| Empty map | `#set ($map = {})` |  |
| Inline map | `#set ($map = {"k": "v"})` | Preserves insertion order |
| Add / update | `#set ($void = $map.put("k", "v"))` | Returns old value; suppress with `$void` |
| Read (literal key) | `$map.get("k")` or `$map["k"]` |  |
| Read (variable key) | `$map.get($key)` |  |
| Dot notation | `$map.key` | Fails for hyphenated keys |
| Iterate entries | `#foreach ($e in $map.entrySet())` | `$e.key`, `$e.value` |
| Iterate keys only | `#foreach ($k in $map.keySet())` |  |
| Check key exists | `$map.containsKey("k")` | Returns boolean |
| Size | `$map.size()` |  |

## Building collections in loops

The most common use of hashmaps and lists in Cascade is building them up inside a loop over query results, then rendering the collected data separately.

### Using a list to collect filtered results

Filter results before rendering by accumulating matches into a list. This decouples data collection from presentation.

```velocity
#set ($upcomingEvents = [])
#set ($now = $_DateTool.getDate())

#set ($results = $_.query().byContentType("Event").preloadDynamicMetadata().sortBy("startDate").sortDirection("asc").maxResults(100).execute())

#foreach ($page in $results)
    #set ($startMillis = $page.metadata.startDate)
    #if ($startMillis && $startMillis > 0)
        #set ($startDate = $_DateTool.getDate($startMillis))
        #set ($diff = $_DateTool.difference($now, $startDate))

        #if ($diff.days >= 0)
            #set ($void = $upcomingEvents.add($page))
        #end
    #end
#end

## Render after filtering
#if ($upcomingEvents.size() > 0)
    <ul>
    #foreach ($event in $upcomingEvents)
        <li>$event.metadata.title</li>
    #end
    </ul>
#else
    <p>No upcoming events.</p>
#end
```

Collect featured items from a query, then limit display to the first three:

```velocity
#set ($featured = [])

#set ($all = $_.query().byContentType("News").preloadStructuredData().sortBy("startDate").sortDirection("desc").maxResults(100).execute())

#foreach ($page in $all)
    #if ($page.getStructuredDataNode("is-featured").textValue == "Yes")
        #set ($void = $featured.add($page))
    #end
#end

## Show only the first 3 featured items
#foreach ($page in $featured)
    #if ($foreach.count > 3)#break#end
    <article>
        <h3>$page.metadata.title</h3>
        <p>$page.metadata.summary</p>
    </article>
#end
```

### Building a hashmap dynamically

Count pages per content type from a query:

```velocity
#set ($typeCounts = {})

#set ($results = $_.query().byFolderPath("/news").bySiteName($currentPage.siteName).maxResults(-1).execute())

#foreach ($page in $results)
    #set ($type = $page.contentTypeName)

    ## Get current count, default to 0
    #set ($count = $typeCounts.get($type))
    #if (!$count)
        #set ($count = 0)
    #end

    ## Increment and store
    #set ($n = $count + 1)
    #set ($void = $typeCounts.put($type, $n))
#end

## Output the breakdown
<ul>
#foreach ($entry in $typeCounts.entrySet())
    <li>$entry.key: $entry.value</li>
#end
</ul>
```

Build a slug-to-page lookup map for fast retrieval later in the template:

```velocity
## Build a map of slug -> page for O(1) lookup
#set ($pageBySlug = {})

#set ($results = $_.query().byContentType("Department").preloadStructuredData().maxResults(100).execute())

#foreach ($page in $results)
    #set ($slug = $page.getStructuredDataNode("slug").textValue)
    #set ($void = $pageBySlug.put($slug, $page))
#end

## Later: look up a page by slug without another query
#set ($dept = $pageBySlug.get("engineering"))
#if ($dept)
    <a href="$dept.link">$dept.metadata.title</a>
#end
```

## Nesting

You can nest lists inside maps and maps inside lists to create richer data structures. These patterns are the Velocity equivalent of JSON arrays of objects and grouped records.

### List of hashmaps

Each item in the list is itself a map. This is the natural structure when collecting structured records—equivalent to an array of objects.

```velocity
#set ($staff = [])

#set ($results = $_.query().byContentType("Staff-Bio").preloadStructuredData().sortBy("name").sortDirection("asc").execute())

#foreach ($page in $results)
    #set ($person = {
        "name":       $page.getStructuredDataNode("last-name").textValue,
        "title":      $page.getStructuredDataNode("job-title").textValue,
        "department": $page.metadata.getDynamicField("department").value,
        "link":       $page.link
    })
    #set ($void = $staff.add($person))
#end

## Render
<ul>
#foreach ($person in $staff)
    <li>
        <a href="$person.get('link')">$person.get('name')</a>
        &mdash; $person.get('title')
    </li>
#end
</ul>
```

###  Hashmap with list values (grouping pattern)

The most powerful nesting pattern: a map where each value is a list. Use it to group pages by department, content type, tag, or any categorical field.

```velocity
## Group events by department
#set ($departments = ["College of Arts", "College of Sciences", "College of Engineering"])

## Initialize each department key with an empty list
#set ($grouped = {})
#foreach ($dept in $departments)
    #set ($void = $grouped.put($dept, []))
#end

## Populate from query
#set ($events = $_.query().byContentType("Event").preloadDynamicMetadata().preloadStructuredData().maxResults(-1).execute())

#foreach ($event in $events)
    #set ($dept = $event.metadata.getDynamicField("department").value)
    #if ($grouped.containsKey($dept))
        #set ($deptList = $grouped.get($dept))
        #set ($void = $deptList.add($event))
    #end
#end

## Render grouped output
#foreach ($entry in $grouped.entrySet())
    #if ($entry.value.size() > 0)
        <section>
            <h2>$entry.key</h2>
            <ul>
            #foreach ($event in $entry.value)
                <li>
                    <a href="$event.link">$event.metadata.title</a>
                </li>
            #end
            </ul>
        </section>
    #end
#end
```

> **Tip**
>
> `.add()` works but also returns a boolean. Without `#set`, Velocity prints that value straight into your page output. Wrapping the call in `#set ($void = ...)` captures the return into a throwaway variable so nothing is rendered. The variable name `$void` is just a convention—any name works—but it signals that you are intentionally discarding the result.

## Tips and gotchas

###  Suppressing output from .put() and .add()

Both `.put()` and `.add()` return a value. If you call them without capturing that return, Velocity prints it into your output.

```velocity
## Wrong - prints "true" or the old value into your page
$map.put("key", "value")
$list.add("item")

## Correct - capture the return with $void
#set ($void = $map.put("key", "value"))
#set ($void = $list.add("item"))
```

### Dot notation fails for hyphenated keys

```velocity
#set ($map = {"show-nav": "true"})

## Fails - Velocity interprets this as ($map.show) minus (nav)
## $map.show-nav

## Correct
$map.get("show-nav")   ## true
$map["show-nav"]       ## true
```

### Null access and the quiet reference

Accessing a key that does not exist returns `null`. Velocity renders `null` references as the literal variable text (e.g. `$map.get("missing")` prints that string). Use the quiet reference `$!` to suppress null output.

```velocity
#set ($map = {"color": "blue"})

## Prints literal "$map.get("missing")" because the key doesn't exist
$map.get("missing")

## Prints nothing - quiet reference suppresses null
$!map.get("missing")

## Guard with containsKey for conditional logic
#if ($map.containsKey("color"))
    Color is: $map.get("color")
#end
```

### Arithmetic inside method arguments

Velocity does not evaluate arithmetic expressions inside method arguments. Always compute the value with `#set` first.

```velocity
## Wrong - arithmetic inside .put() does not work
#set ($void = $map.put("count", $old + 1))

## Correct - compute first, then store
#set ($n = $old + 1)
#set ($void = $map.put("count", $n))
```

### #set does not unset

Once a variable is set, `#set ($var = null)` does **not** clear it in Velocity. The variable retains its previous value. If you need to reset a variable inside a loop, set it to an empty string or an appropriate default before each iteration.

```velocity
#set ($label = "default")

## This does NOT clear $label - it still equals "default"
#set ($label = $nullReference)

## Instead, reset to a known value
#set ($label = "")
```
