{"id":168452,"date":"2026-06-05T00:19:07","date_gmt":"2026-06-04T21:19:07","guid":{"rendered":"https:\/\/computingforgeeks.com\/?p=168452"},"modified":"2026-06-05T00:19:07","modified_gmt":"2026-06-04T21:19:07","slug":"ansible-filters","status":"publish","type":"post","link":"https:\/\/computingforgeeks.com\/ansible-filters\/","title":{"rendered":"Ansible Filters: Transform Data in Playbooks With Real Examples"},"content":{"rendered":"<p>An Ansible filter runs on the control node, inside Jinja2, before a single task touches a managed host. Whatever the filter chain produces is the exact value the task applies. So when a template renders the wrong port or a loop iterates the wrong list, the cause is almost always upstream in a filter expression, not in the module. This guide works through the Ansible filters that do the real data shaping, every one run on a live control node with the output printed so you can see precisely what each transformation returns.<\/p>\n\n<p>The set covered here is the set you reach for on the job: null-safety with <code>default<\/code> and <code>ternary<\/code>, list work with <code>map<\/code>, <code>selectattr<\/code> and set algebra, dictionary merges with <code>combine<\/code>, type casting, hashing and serialization, and the two filters that live in collections, <code>json_query<\/code> and <code>ipaddr<\/code>. The last two sections chain them into one production-style transform and catalog the errors they throw when an input is wrong.<\/p>\n\n<p><em>Tested June 2026 on a Rocky Linux 10 control node (ansible-core 2.16.16, community.general 10.7.9, ansible.utils 5.1.2).<\/em><\/p>\n\n<h2>Where Ansible filters run, and why it matters<\/h2>\n\n<p>A filter is the part of a Jinja2 expression after the pipe: <code>value | filter(args)<\/code>. Ansible evaluates it on the control node when it templates each task, the same engine that renders your <a href=\"https:\/\/computingforgeeks.com\/ansible-jinja2-templates-tutorial\/\" target=\"_blank\" rel=\"noreferrer noopener\">Jinja2 template files<\/a>. The managed host never sees the expression, only the result. That explains most filter confusion: a filter only sees data already available when the task is templated, such as variables, facts gathered earlier in the play, and registered results. It cannot read live state on the remote host, because the value is computed locally before the module is ever sent.<\/p>\n\n<p>Filters chain left to right. <code>list | first | upper<\/code> takes the first element, then uppercases it. Each stage hands its output to the next, so reading a long expression is a matter of reading the pipes in order. Most of the work in a real playbook is composing three or four small filters into one expression that turns raw inventory data into something a module can consume.<\/p>\n\n<h2>Step 1: Build a one-file test harness<\/h2>\n\n<p>The fastest way to learn a filter is to print what it returns. A play targeting <code>localhost<\/code> with <code>debug<\/code> tasks needs no inventory and no remote host, so it runs in well under a second. Save this as <code>harness.yml<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>- name: Filter scratchpad\n  hosts: localhost\n  connection: local\n  gather_facts: false\n  tasks:\n    - debug:\n        msg: \"{{ [3, 1, 2] | sort }}\"<\/code><\/pre>\n\n\n<p>Run it the same way every time. The control node here is Rocky Linux 10 with ansible-core from the distro repos:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>ansible-playbook harness.yml<\/code><\/pre>\n\n\n<p>The version and the two collections used later confirm the environment the rest of this guide was tested against:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"392\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-version-collection-list-rocky-10.png\" alt=\"ansible --version and collection list on a Rocky Linux 10 control node\" class=\"wp-image-168447\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-version-collection-list-rocky-10.png 2560w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-version-collection-list-rocky-10-300x46.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-version-collection-list-rocky-10-1024x157.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-version-collection-list-rocky-10-768x118.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-version-collection-list-rocky-10-1536x235.png 1536w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-version-collection-list-rocky-10-2048x314.png 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><\/figure>\n\n\n<p>Swap the expression inside <code>msg<\/code> and re-run. Every filter in this article was validated with exactly this loop.<\/p>\n\n<h2>Step 2: Defaults and null safety<\/h2>\n\n<p>The <code>default<\/code> filter supplies a value when a variable is undefined. Pass <code>true<\/code> as the second argument to also catch empty strings, which is what you usually want for user-supplied input:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>msg: \"port={{ user_port | default('8080', true) }} timeout={{ missing_var | default(10) }}\"<\/code><\/pre>\n\n\n<p>With <code>user_port<\/code> set to an empty string and <code>missing_var<\/code> never defined, both fall through to their defaults:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>\"msg\": \"port=8080 timeout=10\"<\/code><\/pre>\n\n\n<p>The <code>ternary<\/code> filter turns a boolean into one of two values, with an optional third arm for <code>None<\/code>. It reads cleaner than a Jinja2 <code>if\/else<\/code> when the result is a simple value:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>msg: \"state={{ feature_on | ternary('enabled', 'disabled') }}\"\n# feature_on = true  ->  \"state=enabled\"\n\nmsg: \"{{ maybe_none | ternary('yes', 'no', 'unset') }}\"\n# maybe_none = null  ->  \"unset\"<\/code><\/pre>\n\n\n<p>For parameters rather than values, <code>default(omit)<\/code> is the one to know. It removes the argument entirely, so the module falls back to its own default instead of receiving an empty value. Use it on optional module parameters such as <code>owner<\/code>, <code>group<\/code>, or <code>mode<\/code>. These null-safety filters pair naturally with the patterns in the <a href=\"https:\/\/computingforgeeks.com\/ansible-variables-tutorial\/\" target=\"_blank\" rel=\"noreferrer noopener\">Ansible variables guide<\/a>, where precedence decides which value actually reaches the filter.<\/p>\n\n<h2>Step 3: String filters<\/h2>\n\n<p>Strings are where most config values start. The basics are case and whitespace. <code>trim<\/code> strips surrounding spaces, the case filters do what their names say:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>msg: \"{{ '  Prod-Web-01.Example.COM  ' | trim | lower }}\"<\/code><\/pre>\n\n\n<p>The result is trimmed and lowercased in one pass:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>\"msg\": \"prod-web-01.example.com\"<\/code><\/pre>\n\n\n<p>For substitution, <code>replace<\/code> handles literal text and <code>regex_replace<\/code> handles patterns. One detail catches people: <code>regex_replace<\/code> is case-sensitive by default, so a lowercase pattern will not match mixed-case input unless you pass <code>ignorecase=True<\/code>. The contrast, measured on the same string:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>msg: \"{{ 'Prod-Web-01.Example.COM' | regex_replace('\\\\.example\\\\.com$', '', ignorecase=True) }}\"\n# -> \"Prod-Web-01\"\n\nmsg: \"{{ 'Prod-Web-01.Example.COM' | regex_replace('\\\\.example\\\\.com$', '') }}\"\n# -> \"Prod-Web-01.Example.COM\"   (no match, returned unchanged)<\/code><\/pre>\n\n\n<p>The second form returns the string untouched because the lowercase pattern never matched <code>.Example.COM<\/code>. That silent no-op is a frequent source of &#8220;my regex did nothing&#8221; reports. Note the doubled backslashes in the pattern. Inside a double-quoted YAML value the pattern needs <code>\\\\.<\/code> and <code>\\\\d<\/code>, because YAML rejects a lone <code>\\.<\/code> or <code>\\d<\/code> with &#8220;found unknown escape character&#8221; and refuses to load the playbook. Wrap the value in single quotes instead and single backslashes work.<\/p>\n\n<p>To split and reassemble, <code>split<\/code> turns a delimited string into a list and <code>join<\/code> turns a list back into a string:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>msg: \"{{ 'nginx,redis,postgres' | split(',') }}\"\n# -> [\"nginx\", \"redis\", \"postgres\"]\n\nmsg: \"{{ ['a', 'b', 'c'] | join(' -> ') }}\"\n# -> \"a -> b -> c\"<\/code><\/pre>\n\n\n<p>For extraction, <code>regex_search<\/code> returns the first match and <code>regex_findall<\/code> returns every match. Pulling an IP out of a log line is the canonical case:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>msg: \"ip={{ logline | regex_search('\\\\d+\\\\.\\\\d+\\\\.\\\\d+\\\\.\\\\d+') }}\"<\/code><\/pre>\n\n\n<p>Against a real log line the search returns the address and nothing else:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>\"msg\": \"ip=10.0.1.7\"<\/code><\/pre>\n\n\n<p>Both regex filters use Python regular-expression syntax, so the patterns you already know carry straight over. Pass a capture-group backreference such as <code>'\\1'<\/code> right after the pattern to pull out just that group instead of the whole match; the result comes back as a list of the captured pieces.<\/p>\n\n<h2>Step 4: List filters<\/h2>\n\n<p>List filters are the workhorses. The cleanup pair is <code>unique<\/code> and <code>sort<\/code>, which deduplicate and order in the obvious way. The aggregate filters <code>min<\/code>, <code>max<\/code>, and <code>length<\/code> answer the questions you would otherwise write a loop for.<\/p>\n\n<p>The two that change how you write playbooks are <code>map<\/code> and <code>selectattr<\/code>. <code>map(attribute=...)<\/code> pulls one field out of every dictionary in a list. <code>selectattr<\/code> keeps only the dictionaries whose attribute passes a test, and <code>rejectattr<\/code> drops them. Given a list of host dictionaries, extracting names and filtering by role is a single expression each:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>msg: \"{{ servers | map(attribute='name') | list }}\"\n# -> [\"web01\", \"web02\", \"db01\"]\n\nmsg: \"{{ servers | selectattr('role', 'equalto', 'web') | map(attribute='name') | list }}\"\n# -> [\"web01\", \"web02\"]\n\nmsg: \"{{ servers | rejectattr('cpu', 'lt', 8) | map(attribute='name') | list }}\"\n# -> [\"web02\", \"db01\"]<\/code><\/pre>\n\n\n<p>Set algebra works directly on lists. <code>union<\/code>, <code>intersect<\/code>, and <code>difference<\/code> compare two lists and return the combined, common, or left-only elements:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>msg: \"union={{ a | union(b) }} intersect={{ a | intersect(b) }} difference={{ a | difference(b) }}\"<\/code><\/pre>\n\n\n<p>With <code>a=[1,2,3,4]<\/code> and <code>b=[3,4,5,6]<\/code> the three operations resolve as expected:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>\"msg\": \"union=[1, 2, 3, 4, 5, 6] intersect=[3, 4] difference=[1, 2]\"<\/code><\/pre>\n\n\n<p>Running the full list playbook shows the dictionary reshaping and the set operations together. This output is what feeds the conditionals and iteration covered in the <a href=\"https:\/\/computingforgeeks.com\/ansible-conditionals-loops-tutorial\/\" target=\"_blank\" rel=\"noreferrer noopener\">conditionals and loops guide<\/a>:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"902\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-list-filters-selectattr-map.png\" alt=\"Ansible selectattr and map filters reshaping a list of host dictionaries\" class=\"wp-image-168448\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-list-filters-selectattr-map.png 2560w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-list-filters-selectattr-map-300x106.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-list-filters-selectattr-map-1024x361.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-list-filters-selectattr-map-768x271.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-list-filters-selectattr-map-1536x541.png 1536w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-list-filters-selectattr-map-2048x722.png 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><\/figure>\n\n\n<p>Two more are worth keeping in reach. <code>zip<\/code> pairs two lists element by element, and <code>flatten<\/code> collapses nested lists into one level. Both turn up constantly when you are stitching parallel data together.<\/p>\n\n<h2>Step 5: Dictionary filters<\/h2>\n\n<p>Dictionaries need different tools because you often have to iterate them or merge them. <code>dict2items<\/code> converts a map into a list of <code>key<\/code>\/<code>value<\/code> pairs, which is the only way to loop over a dictionary in Ansible. <code>items2dict<\/code> does the reverse:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>msg: \"{{ {'cpu': '2', 'mem': '512Mi'} | dict2items }}\"\n# -> [{\"key\": \"cpu\", \"value\": \"2\"}, {\"key\": \"mem\", \"value\": \"512Mi\"}]<\/code><\/pre>\n\n\n<p>The merge filter is <code>combine<\/code>. It overlays one dictionary on another, and the right-hand side wins on any key collision. This is how you express a base config plus per-environment overrides:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>msg: \"{{ base_cfg | combine(override_cfg) }}\"<\/code><\/pre>\n\n\n<p>With a base of <code>{port: 8080, tls: false, workers: 2}<\/code> and an override of <code>{tls: true, workers: 8}<\/code>, the merge keeps the untouched key and replaces the rest:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>\"msg\": {\"port\": 8080, \"tls\": true, \"workers\": 8}<\/code><\/pre>\n\n\n<p>By default <code>combine<\/code> merges only the top level. Pass <code>recursive=True<\/code> to merge nested dictionaries instead of replacing them wholesale, which matters when your override touches one key inside a larger sub-dictionary.<\/p>\n\n<h2>Step 6: Numbers and type casting<\/h2>\n\n<p>Values that arrive as strings need casting before arithmetic. <code>int<\/code> and <code>float<\/code> convert, and they fail loudly on garbage rather than guessing. <code>round<\/code> controls precision:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>msg: \"sum={{ ('42' | int) + 8 }} round2={{ 3.14159 | round(2) }}\"\n# -> \"sum=50 round2=3.14\"<\/code><\/pre>\n\n\n<p>Two filters earn their place in storage and capacity work. <code>human_readable<\/code> formats a byte count, and <code>human_to_bytes<\/code> parses a size string back into bytes:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>msg: \"{{ 5368709120 | human_readable }}\"     # -> \"5.00 GB\"\nmsg: \"{{ '2.5 GB' | human_to_bytes }}\"       # -> 2684354560<\/code><\/pre>\n\n\n<p>The <code>int<\/code> filter also takes a <code>base<\/code> argument, so parsing hex or binary strings needs no external tool: <code>'0x1F' | int(base=16)<\/code> returns <code>31<\/code>.<\/p>\n\n<h2>Step 7: Encoding, hashing, and serialization<\/h2>\n\n<p>These filters move data between formats. <code>b64encode<\/code> and <code>b64decode<\/code> handle Base64, <code>hash<\/code> produces a checksum, and <code>password_hash<\/code> generates a crypt-format hash suitable for an <code>\/etc\/shadow<\/code> entry. The base64 round trip and a SHA1 sum:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>msg: \"enc={{ secret | b64encode }} dec={{ secret | b64encode | b64decode }}\"\n# -> \"enc=UzNjcjN0LVRva2Vu dec=S3cr3t-Token\"\n\nmsg: \"{{ secret | hash('sha1') }}\"\n# -> \"216d5c9034bf958b45179942c9339ae0f0df327c\"<\/code><\/pre>\n\n\n<p>For structured data, <code>to_json<\/code>, <code>to_nice_json<\/code>, and <code>to_nice_yaml<\/code> serialize a variable, and <code>from_json<\/code> parses a JSON string back into data you can index. The nice variants add indentation for readable config output:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>msg: \"{{ ('{\\\"a\\\": 1, \\\"b\\\": [2, 3]}' | from_json).b }}\"\n# -> [2, 3]<\/code><\/pre>\n\n\n<p>Keep secrets out of <code>debug<\/code> output in real plays. The token above is a throwaway value; in production you would pull it from <a href=\"https:\/\/computingforgeeks.com\/ansible-vault-tutorial\/\" target=\"_blank\" rel=\"noreferrer noopener\">an encrypted Vault variable<\/a> and never print it.<\/p>\n\n<h2>Step 8: Filters that live in collections<\/h2>\n\n<p>Not every filter ships with ansible-core. Two of the most useful are packaged in collections and must be installed first. <code>json_query<\/code> comes from <code>community.general<\/code> and runs JMESPath queries against nested data. <code>ipaddr<\/code> comes from <code>ansible.utils<\/code> and validates or slices IP addresses. Install both, the same way the <a href=\"https:\/\/computingforgeeks.com\/ansible-collections-tutorial\/\" target=\"_blank\" rel=\"noreferrer noopener\">collections guide<\/a> covers in depth:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>ansible-galaxy collection install community.general ansible.utils\npip3 install --user jmespath netaddr<\/code><\/pre>\n\n\n<p>The two Python libraries are not optional. <code>json_query<\/code> needs <code>jmespath<\/code> and <code>ipaddr<\/code> needs <code>netaddr<\/code>; without them the filters fail at runtime with an explicit message, shown in the troubleshooting section. With the libraries present, a JMESPath query pulls every running pod name out of a Kubernetes-style structure, and <code>ipaddr<\/code> dissects a CIDR:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>msg: \"{{ pods | community.general.json_query(\\\"items[?status.phase=='Running'].metadata.name\\\") }}\"\n# -> [\"web-1\", \"db-1\"]\n\nmsg: \"network={{ '10.0.1.0\/24' | ansible.utils.ipaddr('network') }} netmask={{ '10.0.1.0\/24' | ansible.utils.ipaddr('netmask') }}\"\n# -> \"network=10.0.1.0 netmask=255.255.255.0\"<\/code><\/pre>\n\n\n<p>Both filters resolved correctly once the libraries were in place:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"670\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-json-query-ipaddr-collection-filters.png\" alt=\"Ansible json_query and ipaddr filters from community.general and ansible.utils\" class=\"wp-image-168449\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-json-query-ipaddr-collection-filters.png 2560w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-json-query-ipaddr-collection-filters-300x79.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-json-query-ipaddr-collection-filters-1024x268.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-json-query-ipaddr-collection-filters-768x201.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-json-query-ipaddr-collection-filters-1536x402.png 1536w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-json-query-ipaddr-collection-filters-2048x536.png 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><\/figure>\n\n\n<p>Call collection filters by their fully qualified name, <code>community.general.json_query<\/code> rather than bare <code>json_query<\/code>. The short name still resolves through the collection search path, but the FQCN states exactly which collection owns the filter and will not collide if another collection ever ships one with the same name.<\/p>\n\n<h2>Step 9: Chain filters into one transform<\/h2>\n\n<p>The payoff is composing filters into a single expression that turns raw data into something a template can drop straight into a config file. Take a fleet of host dictionaries and produce load-balancer server lines: keep the web role, drop any host marked for draining, sort by weight, then format each survivor into a config line. That is five filters in one pipe:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>lines: \"{{ fleet\n  | selectattr('role', 'equalto', 'web')\n  | rejectattr('drain')\n  | sort(attribute='weight', reverse=True)\n  | map(attribute='ip')\n  | map('regex_replace', '^', 'server ')\n  | map('regex_replace', '$', ':8080 check')\n  | list }}\"<\/code><\/pre>\n\n\n<p>The two anchored <code>regex_replace<\/code> stages prepend and append text without a backreference, which sidesteps the escaping problem entirely. The draining host drops out and the remaining two render as finished config lines:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"808\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-chained-filters-upstream-pool.png\" alt=\"Chained Ansible filters transforming fleet data into upstream server lines\" class=\"wp-image-168450\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-chained-filters-upstream-pool.png 2560w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-chained-filters-upstream-pool-300x95.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-chained-filters-upstream-pool-1024x323.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-chained-filters-upstream-pool-768x242.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-chained-filters-upstream-pool-1536x485.png 1536w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-ansible-chained-filters-upstream-pool-2048x646.png 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><\/figure>\n\n\n<p>Building the same result with tasks and registered variables would take a dozen lines and a loop. The filter chain does it in one expression that you can read top to bottom. That density is the reason filters are worth learning properly rather than reaching for a custom module.<\/p>\n\n<h2>Troubleshooting filter errors<\/h2>\n\n<p>Filter failures are usually clear once you have seen them once. These are the errors captured during testing, with the exact text and the fix.<\/p>\n\n<h3>Error: &#8220;You need to install \\&#8221;jmespath\\&#8221; prior to running json_query filter&#8221;<\/h3>\n\n<p>The <code>json_query<\/code> filter is installed but its Python backend is missing. Install it on the control node, because the filter runs there: <code>pip3 install --user jmespath<\/code>. The matching message for <code>ipaddr<\/code> names <code>netaddr<\/code> instead; the fix is the same.<\/p>\n\n<h3>Error: &#8220;&#8216;dict object&#8217; has no attribute &#8216;name'&#8221;<\/h3>\n\n<p>A <code>map(attribute='name')<\/code> or <code>selectattr('name', ...)<\/code> hit a dictionary that lacks that key. Either the data is inconsistent or the attribute name is misspelled. Guard it with a default inside the map, or filter the list first so only well-formed items reach the attribute access.<\/p>\n\n<h3>A regex_replace returns the string unchanged<\/h3>\n\n<p>The pattern did not match, so the filter returned the input verbatim. The usual cause is a case mismatch, fixed with <code>ignorecase=True<\/code>. The <a href=\"https:\/\/computingforgeeks.com\/ansible-debugging\/\" target=\"_blank\" rel=\"noreferrer noopener\">playbook debugging guide<\/a> covers isolating expressions like this with a throwaway <code>debug<\/code> task.<\/p>\n\n<h3>Error: &#8220;found unknown escape character&#8221;<\/h3>\n\n<p>YAML raised this before Ansible ran anything. A regex inside a double-quoted value used a single backslash, such as <code>'\\d+'<\/code> inside <code>msg: \"...\"<\/code>. Double the backslashes to <code>'\\\\d+'<\/code>, or switch the outer value to single quotes and keep the single backslash. The playbook will not load until the escape is valid YAML.<\/p>\n\n<h3>Error: &#8220;template error while templating string &#8230; no filter named&#8221;<\/h3>\n\n<p>The filter name is wrong or it lives in a collection that is not installed. Check the spelling, and for collection filters confirm the collection is present with <code>ansible-galaxy collection list<\/code> and call the filter by its full <code>namespace.collection.filter<\/code> name.<\/p>\n\n<h2>Filter source at a glance<\/h2>\n\n<p>The single most common stumble is not knowing whether a filter ships with ansible-core or needs a collection plus a Python library. This table sorts the filters used above by where they come from:<\/p>\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Filter<\/th><th>Source<\/th><th>Extra requirement<\/th><\/tr><\/thead><tbody>\n<tr><td><code>default<\/code>, <code>ternary<\/code>, <code>map<\/code>, <code>selectattr<\/code>, <code>combine<\/code>, <code>regex_replace<\/code><\/td><td>ansible-core<\/td><td>none<\/td><\/tr>\n<tr><td><code>human_readable<\/code>, <code>human_to_bytes<\/code>, <code>dict2items<\/code><\/td><td>ansible-core<\/td><td>none<\/td><\/tr>\n<tr><td><code>b64encode<\/code>, <code>hash<\/code>, <code>password_hash<\/code>, <code>to_nice_yaml<\/code><\/td><td>ansible-core<\/td><td>none<\/td><\/tr>\n<tr><td><code>community.general.json_query<\/code><\/td><td>community.general<\/td><td><code>jmespath<\/code> (pip)<\/td><\/tr>\n<tr><td><code>ansible.utils.ipaddr<\/code><\/td><td>ansible.utils<\/td><td><code>netaddr<\/code> (pip)<\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n<p>Keep the core filters in muscle memory and the table above bookmarked for the rest. For a denser one-page reference to the whole language, the <a href=\"https:\/\/computingforgeeks.com\/ansible-cheat-sheet\/\" target=\"_blank\" rel=\"noreferrer noopener\">Ansible cheat sheet<\/a> collects the filter and module syntax in one place, and the full learning path lives in the <a href=\"https:\/\/computingforgeeks.com\/ansible-automation-guide\/\" target=\"_blank\" rel=\"noreferrer noopener\">Ansible automation guide<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>An Ansible filter runs on the control node, inside Jinja2, before a single task touches a managed host. Whatever the filter chain produces is the exact value the task applies. So when a template renders the wrong port or a loop iterates the wrong list, the cause is almost always upstream in a filter expression, &#8230; <a title=\"Ansible Filters: Transform Data in Playbooks With Real Examples\" class=\"read-more\" href=\"https:\/\/computingforgeeks.com\/ansible-filters\/\" aria-label=\"Read more about Ansible Filters: Transform Data in Playbooks With Real Examples\">Read more<\/a><\/p>\n","protected":false},"author":21,"featured_media":168451,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[606,329],"tags":[314,212,669],"cfg_series":[39825],"class_list":["post-168452","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ansible","category-automation","tag-ansible","tag-automation","tag-dev","cfg_series-ansible-mastery"],"_links":{"self":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/168452","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/users\/21"}],"replies":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/comments?post=168452"}],"version-history":[{"count":1,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/168452\/revisions"}],"predecessor-version":[{"id":168453,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/168452\/revisions\/168453"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media\/168451"}],"wp:attachment":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media?parent=168452"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/categories?post=168452"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/tags?post=168452"},{"taxonomy":"cfg_series","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/cfg_series?post=168452"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}