Chapter 6: Jinja2 Templating
Topic 35

Filters

TemplatingFilters

A filter transforms a value inside {{ }} with a pipe: {{ port | default(8080) }} substitutes a fallback when port is not set. Filters are the workhorses of real templates and playbooks. They supply defaults, convert types, reshape lists, and enforce that a required value exists — and they chain left to right, so one expression can default a value, transform it, and serialize it in a single pass.

The handful covered here are the ones you reach for daily writing the Larkspur config files: default and its omit variant for optional settings, mandatory for values the app cannot start without, the to_json family for serializing structured data, and the list filters that turn inventory into config lines. Learn these five well and most template expressions you ever write are some combination of them.

Three workhorse filters and what each does
default
Supplies a fallback for an optional value — {{ nginx_workers | default(4) }} fills in when the variable is unset.
mandatory
Fails the play loudly if the value is undefined — for a must-exist secret like the database password the app cannot start without.
to_json
Serializes a structure into valid JSON — {{ settings | to_nice_json }} instead of leaking Python's repr into the file.

default — the Optional-Value Filter

{{ nginx_workers | default(4) }} fills in a value when the variable is undefined, which is the everyday case for an optional setting with a sane fallback. There is one wrinkle: plain default only fires on undefined, not on an empty string. A variable set to "" sails right past it. To catch the empty case too, pass a second argument — {{ x | default('', true) }} substitutes on both undefined and falsy values.

That distinction trips people constantly. You set a variable to an empty string expecting the default to kick in, and instead the empty string renders straight into the config. The fix is the true flag, which widens default from "undefined only" to "undefined or empty." Knowing which behavior you want — strictly-unset versus also-empty — is the difference between a config that fills in correctly and one with a blank where a value should be.

default(omit) — the Disappearing Argument

The special omit placeholder makes a module not pass a parameter at all. mode: "{{ file_mode | default(omit) }}" means "use my explicit file_mode if it is set, otherwise let the module choose its own default." That is how you write one task that takes an optional argument without splitting into two near-identical tasks — one with the parameter, one without.

Optional permissions with default(omit) versus an empty string
# omit removes the parameter when file_mode is unset —
# the module picks its own default mode
mode: "{{ file_mode | default(omit) }}"

# default('') passes a LITERAL empty string — almost never what you want
# for mode or owner; it sets the argument to empty, not absent
mode: "{{ file_mode | default('') }}"

The trap is confusing default(omit) with default(''). The first removes the parameter entirely; the second passes an empty string as its value. Using '' for a mode or owner argument sets it to empty rather than letting the module decide, which is a different and usually broken outcome. When the answer is "leave this argument off," the filter is omit, never an empty string.

mandatory — Fail Loud on a Missing Value

{{ vault_db_password | mandatory }} fails the play with a clear error if the variable is undefined. That turns a silent empty-string render of app.env into a loud, early failure — the right move for a value the application cannot run without. The contrast with default is the whole point: default papers over a missing value, mandatory refuses to proceed without it.

The choice between them is a judgment about what "missing" should mean for each value. An optional tuning knob like worker count gets a default; a required secret like the database password or the session key gets mandatory. Reaching for default('') on a required value is the worst of both — the app boots misconfigured with a blank where the secret should be, and you find out from a runtime error instead of a failed play.

Type Conversions

to_json and from_json, to_yaml, to_nice_json, bool, and int serialize and coerce values. The case that bites is embedding structured data in a config: writing {{ settings }} for an Ansible dict emits Python's repr — single quotes, True, None — which is not valid JSON. {{ settings | to_nice_json }} produces properly indented, valid JSON instead. Any time a parser will read the output, run the structure through a serializer rather than letting the language's native repr leak into the file.

List Filters and Chaining

map('attribute', 'name') pulls one field from a list of dicts, select and reject filter by a test, and join(',') flattens a list to a string. Chained, they build a config line from inventory data in one expression — {{ groups['web'] | map('extract', hostvars, 'ansible_host') | join(' ') }} turns the web group into a space-separated list of IPs. The manual alternative, a set_fact and a loop to extract one field, is more code and more places to break for a result one chained expression delivers.

Common Mistakes
  • Writing {{ secret_key | default('') }} for a value the app requires, so an unset variable renders an empty string and the app boots misconfigured instead of failing — use | mandatory for must-exist values.
  • Confusing | default(omit) with | default('')omit removes the parameter entirely, while '' passes an empty string; using '' for a mode or owner sets it to empty rather than letting the module choose.
  • Forgetting default(value, true) and being surprised a variable set to "" does not trigger the default — plain default fires only on undefined, not on empty, unless you pass the second true argument.
  • Embedding an Ansible dict in a config with bare {{ mydict }}, emitting Python's repr — single quotes, True, None — which is not valid JSON; use to_json or to_nice_json for anything a parser reads.
  • Reaching for a set_fact and a loop to extract one field from a list of dicts when map('attribute', 'field') does it in one expression — the manual version is more code and more places to break.
Best Practices
  • Use | mandatory on every variable the rendered file cannot be valid without — the database password, the session key — so a missing value fails the play loudly instead of shipping a broken config.
  • Reach for | default(omit) when a module parameter is optional, writing one task that adapts instead of branching into two near-identical tasks.
  • Serialize structured data into config with to_json, to_nice_json, or to_yaml rather than letting a native repr leak into the file, so the output is always valid for the parser that reads it.
  • Chain list filters — map then select then join — to build config lines from inventory data in a single expression, keeping the derivation in the template instead of scattering set_fact steps.
  • Pass the second true argument to default when an empty string should also trigger the fallback, so a variable set to "" does not render a blank into a live config.
Comparable tools Terraform coalesce / try and jsonencode, the direct analogs Flask / Django templates share the same Jinja2 filters jq does the list reshaping that map and select do here

Knowledge Check

A value the application cannot start without is unset. Which filter handles this correctly?

  • | mandatory — it fails the play with a clear error instead of letting a blank render into the config
  • | default('') — it substitutes an empty string so the app boots with a blank where the secret belongs
  • | default(omit) — it drops the parameter so the module just proceeds without that value
  • | bool — it coerces the missing value to a false the app reads as a disabled flag

What is the difference between | default(omit) and | default('') on a module's mode parameter?

  • omit removes the parameter so the module picks its own default; '' passes a literal empty string as the value
  • They are identical; omit is just a more readable spelling of the empty string '' that the module ends up rendering and applying the exact same way
  • omit sets the mode to a locked-down 0000 with no read or write bits at all, while '' leaves whatever mode the file already had on disk untouched
  • omit works only inside a rendered template file and '' only inside a task's module argument

Why does {{ port | default(8080) }} not fall back when port is set to an empty string?

  • Plain default fires only on undefined, not on empty; you need default(8080, true) to also catch the empty case
  • default never works on string variables, only on numeric ones like a port number, an integer timeout, or a worker count
  • An empty string counts as undefined to the filter, so the 8080 fallback does fire here exactly as it would for an unset variable
  • The fallback must be quoted as the string "8080" rather than left as a bare integer before the filter will engage on an empty value

You need to render an Ansible dict as a config block a JSON parser will read. What do you use?

  • {{ settings | to_nice_json }} — bare interpolation emits Python's repr, which is not valid JSON
  • {{ settings }} — Jinja2 serializes the dict to valid JSON with double quotes automatically
  • {{ settings | join(',') }} — it flattens the dict's keys into a comma-separated JSON object
  • {{ settings | mandatory }} — it validates the dict is set and serializes it in one step

You got correct