How to modify a list of dictionaries with Ansible

I keep running into scenarios with Ansible for which there doesn’t seem a ready StackOverflow or Serverfault answer and which usually prompts a “You must be doing something very strange that Ansible was not made for” answer from my Belgian friends. One of these scenarios is how to modify an existing list of dictionaries with Ansible. As this turned out to be not that trivial, I’ll document it here mostly for my own reference.

If you want to modify a list of strings things are pretty easy. Given

flat_list:
  - "foo"
  - "bar"

you can modify this list quite easily with filters such as map and e.g. regex_replace. But what if you have a list of dictionaries such as the following:

packages:
  - name: "terminfo"
    feed: "openwrt_base"
  - name: "libncurses6"
    feed: "openwrt_base"

Suppose you want to add a third element to each dictionary in this list. If the key and the value are static, things are easy:

{{ packages | map('combine', {'foo': 'bar'}) }}

But what if the key or the value are dynamic, e.g. because they depend on the particular dictionary?

Provided that each dictionary has one identifying property (i.e. name) this can be implemented as follows:

- name: "filenames of newest package versions determined"
  set_fact:
    packages: "{{
      packages |
      rejectattr('name', 'equalto', item['name']) | list +
      [ item | combine({ 'feed_suffixed': '{{ item['feed'] }}_suffixed'}) ]
    }}"
  loop: "{{ packages }}"

Let’s examine this closer: this uses a loop that iterates over each element of the list, i.e. each dictionary. We could say that map also does that behind the scenes but here in our case it’s more explicit. Then, in every loop iteration we use set_fact to redefine the packages variable. In doing so we need to do two things:

  1. first, we need to incorporate into the definition the existing list as-is except for the current element. That’s what the rejectattr line above does, rejecting the one item in packages that has a name attribute equal to the currently iterated item’s name. As said, this only works if each dictionary has such an identifying property. Because rejectattr itself like map returns a generator, we also need to apply the list operator to get a list back.
  2. second, we need to construct the updated version of the current element. Adding another key and value is done by combining the old dictionary with a temporary new directory with just the new key and value into a new dictionary.
  3. third, in order to get that new dictionary into packages we need to wrap it into a list using square brackets because the + operator only works between lists, not a list and a dictionary.