Ansible Jinja2 Filters — map, select, reject, json_query

Introduction

Jinja2 filters transform data inline — extracting fields from lists, filtering objects by attributes, reformatting strings, and querying nested JSON structures. Ansible adds dozens of custom filters beyond standard Jinja2. This guide covers the filters you'll use most often with practical examples.

map — Transform Every Item

    # Extract attribute from list of dicts
    - name: Get all hostnames
      ansible.builtin.debug:
        msg: "{{ users | map(attribute='name') | list }}"
      vars:
        users:
          - { name: alice, role: admin }
          - { name: bob, role: user }
      # Output: ["alice", "bob"]

    # Apply filter to every item
    - name: Uppercase all items
      ansible.builtin.debug:
        msg: "{{ fruits | map('upper') | list }}"
      vars:
        fruits: [apple, banana, cherry]
      # Output: ["APPLE", "BANANA", "CHERRY"]

    # Apply filter with argument
    - name: Prefix all items
      ansible.builtin.debug:
        msg: "{{ names | map('regex_replace', '^', 'user_') | list }}"
      vars:
        names: [alice, bob, charlie]
      # Output: ["user_alice", "user_bob", "user_charlie"]

select / reject — Filter Items

    # Select items matching a test
    - name: Get even numbers
      ansible.builtin.debug:
        msg: "{{ numbers | select('even') | list }}"
      vars:
        numbers: [1, 2, 3, 4, 5, 6]
      # Output: [2, 4, 6]

    # Reject items matching a test
    - name: Remove empty strings
      ansible.builtin.debug:
        msg: "{{ items | reject('equalto', '') | list }}"
      vars:
        items: ["hello", "", "world", "", "!"]
      # Output: ["hello", "world", "!"]

    # Select strings matching pattern
    - name: Get web servers
      ansible.builtin.debug:
        msg: "{{ servers | select('match', 'web.*') | list }}"
      vars:
        servers: [web01, db01, web02, cache01, web03]
      # Output: ["web01", "web02", "web03"]

    # Select numbers greater than
    - name: High values
      ansible.builtin.debug:
        msg: "{{ values | select('gt', 100) | list }}"
      vars:
        values: [50, 150, 75, 200, 30]
      # Output: [150, 200]

selectattr / rejectattr — Filter by Attribute

    # Select dicts where attribute matches
    - name: Get active users
      ansible.builtin.debug:
        msg: "{{ users | selectattr('active', 'equalto', true) | list }}"
      vars:
        users:
          - { name: alice, active: true }
          - { name: bob, active: false }
          - { name: charlie, active: true }
      # Output: [{"name": "alice", "active": true}, {"name": "charlie", "active": true}]

    # Chain selectattr + map
    - name: Get names of admin users
      ansible.builtin.debug:
        msg: "{{ users | selectattr('role', 'equalto', 'admin') | map(attribute='name') | list }}"
      vars:
        users:
          - { name: alice, role: admin }
          - { name: bob, role: user }
          - { name: charlie, role: admin }
      # Output: ["alice", "charlie"]

    # Reject by attribute
    - name: Get non-disabled services
      ansible.builtin.debug:
        msg: "{{ services | rejectattr('disabled', 'defined') | map(attribute='name') | list }}"

json_query (JMESPath)

    # Query nested structures
    - name: Extract from complex JSON
      ansible.builtin.debug:
        msg: "{{ data | json_query('servers[*].name') }}"
      vars:
        data:
          servers:
            - { name: web01, region: us-east, type: t3.micro }
            - { name: web02, region: eu-west, type: t3.small }
            - { name: db01, region: us-east, type: r6g.large }

    # Filter with conditions
    - name: US East servers only
      ansible.builtin.debug:
        msg: "{{ data | json_query(\"servers[?region=='us-east'].name\") }}"
      # Output: ["web01", "db01"]

    # Multiple fields
    - name: Name and type pairs
      ansible.builtin.debug:
        msg: "{{ data | json_query('servers[*].{host: name, size: type}') }}"

    # Nested query
    - name: Get specific value
      ansible.builtin.debug:
        msg: "{{ api_response | json_query('data.items[0].metadata.name') }}"

String Filters

    # regex_replace
    - ansible.builtin.debug:
        msg: "{{ 'hello-world_v2' | regex_replace('[^a-zA-Z0-9]', '_') }}"
      # Output: "hello_world_v2"

    # regex_search (extract match)
    - ansible.builtin.debug:
        msg: "{{ version_string | regex_search('[0-9]+\\.[0-9]+\\.[0-9]+') }}"
      vars:
        version_string: "app-v2.5.1-release"
      # Output: "2.5.1"

    # split and join
    - ansible.builtin.debug:
        msg: "{{ 'a,b,c' | split(',') | join(' | ') }}"
      # Output: "a | b | c"

    # hash
    - ansible.builtin.debug:
        msg: "{{ 'my_password' | hash('sha256') }}"

Collection Filters

    # combine (merge dicts)
    - ansible.builtin.set_fact:
        full_config: "{{ defaults | combine(overrides, recursive=True) }}"
      vars:
        defaults:
          app: { port: 8080, workers: 4 }
          db: { host: localhost }
        overrides:
          app: { port: 9090 }
          db: { host: db.example.com }
      # Result: {app: {port: 9090, workers: 4}, db: {host: "db.example.com"}}

    # unique
    - ansible.builtin.debug:
        msg: "{{ [1, 2, 2, 3, 3, 3] | unique | list }}"
      # Output: [1, 2, 3]

    # flatten
    - ansible.builtin.debug:
        msg: "{{ [[1, 2], [3, 4], [5]] | flatten }}"
      # Output: [1, 2, 3, 4, 5]

    # difference / intersect / union
    - ansible.builtin.debug:
        msg: |
          Diff: {{ [1,2,3] | difference([2,3,4]) }}
          Inter: {{ [1,2,3] | intersect([2,3,4]) }}
          Union: {{ [1,2,3] | union([2,3,4]) }}
      # Diff: [1], Inter: [2, 3], Union: [1, 2, 3, 4]

    # dict2items / items2dict
    - ansible.builtin.debug:
        msg: "{{ my_dict | dict2items }}"
      vars:
        my_dict: { a: 1, b: 2 }
      # Output: [{"key": "a", "value": 1}, {"key": "b", "value": 2}]

Type Conversion

    # Type casting
    "{{ '42' | int }}"           # → 42
    "{{ '3.14' | float }}"       # → 3.14
    "{{ 'yes' | bool }}"         # → true
    "{{ [1, 2] | string }}"      # → "[1, 2]"
    "{{ data | to_json }}"       # → JSON string
    "{{ json_str | from_json }}" # → dict/list
    "{{ data | to_yaml }}"       # → YAML string
    "{{ yaml_str | from_yaml }}" # → dict/list

Chaining Filters (Real Example)

    # Get IPs of running web containers
    - name: Complex filter chain
      ansible.builtin.debug:
        msg: >-
          {{ containers
             | selectattr('state', 'equalto', 'running')
             | selectattr('name', 'match', 'web-.*')
             | map(attribute='network')
             | map(attribute='ip')
             | list
             | join(', ') }}

Troubleshooting

IssueSolution
list not foundAdd | list after map/select (they return generators)
json_query not foundInstall pip install jmespath
Regex backslash issuesDouble-escape in YAML: \\d or use raw strings
Type error in comparisonCast first: | int before comparing
Empty resultCheck attribute names; use default([])

Best Practices

  1. Always | list — after map, select, reject (they return generators)
  2. Use selectattr + map — filter then extract in one chain
  3. Prefer json_query — for deeply nested structures
  4. Use default() — prevent undefined variable errors
  5. Keep chains readable — break long chains across lines with >-
  6. Test in debug — verify filter chains with debug before using in tasks

Conclusion

Jinja2 filters are Ansible's data manipulation toolkit. map and selectattr replace verbose loops, json_query navigates complex API responses, and combine merges configuration layers. Master these filters and you'll write playbooks that handle complex data transformations in single expressions instead of multi-task workarounds.