Ansible from_json and to_json — Parse and Generate JSON

Introduction

Ansible's from_json and to_json filters convert between JSON strings and native data structures. Use from_json to parse API responses and command output. Use to_json to generate JSON for API calls and config files. Both are essential for working with REST APIs and JSON-based tools.

Quick Reference

# Parse JSON string → dict/list
- set_fact:
    data: "{{ json_string | from_json }}"

# Convert dict/list → JSON string
- debug:
    msg: "{{ my_dict | to_json }}"

# Pretty-printed JSON
- debug:
    msg: "{{ my_dict | to_nice_json }}"

from_json — Parse JSON Strings

- name: Parse JSON from command output
  ansible.builtin.command: cat /etc/myapp/config.json
  register: config_raw
  changed_when: false

- name: Convert to dictionary
  ansible.builtin.set_fact:
    config: "{{ config_raw.stdout | from_json }}"

- name: Access parsed values
  ansible.builtin.debug:
    msg: "Database host: {{ config.database.host }}"

Parse API Response

- name: Call REST API
  ansible.builtin.uri:
    url: https://api.example.com/v1/servers
    return_content: true
  register: api_response

# uri module auto-parses JSON, but if you have a raw string:
- name: Parse manually if needed
  ansible.builtin.set_fact:
    servers: "{{ api_response.content | from_json }}"

- name: List server names
  ansible.builtin.debug:
    msg: "{{ servers | map(attribute='name') | list }}"

Parse JSON from Shell Output

- name: Get Docker container info
  ansible.builtin.command: docker inspect mycontainer
  register: docker_info
  changed_when: false

- name: Parse container IP
  ansible.builtin.set_fact:
    container_ip: "{{ (docker_info.stdout | from_json)[0].NetworkSettings.IPAddress }}"

- name: Show IP
  ansible.builtin.debug:
    msg: "Container IP: {{ container_ip }}"

to_json — Generate JSON Strings

- name: Create JSON config file
  ansible.builtin.copy:
    content: "{{ app_config | to_nice_json }}"
    dest: /etc/myapp/config.json
  vars:
    app_config:
      database:
        host: db.example.com
        port: 5432
        name: production
      cache:
        type: redis
        host: redis.example.com
        ttl: 3600

Send JSON to API

- name: Create resource via API
  ansible.builtin.uri:
    url: https://api.example.com/v1/resources
    method: POST
    body: "{{ payload | to_json }}"
    body_format: json
    headers:
      Authorization: "Bearer {{ api_token }}"
  vars:
    payload:
      name: "{{ resource_name }}"
      type: compute
      region: us-east-1
      tags:
        environment: production
        team: platform

to_nice_json — Pretty Print

- name: Readable JSON output
  ansible.builtin.debug:
    msg: "{{ complex_data | to_nice_json }}"
  # Output:
  # {
  #     "name": "myapp",
  #     "version": "2.1.0",
  #     "dependencies": [
  #         "redis",
  #         "postgresql"
  #     ]
  # }

# Control indentation
- debug:
    msg: "{{ data | to_nice_json(indent=2) }}"

from_yaml and to_yaml

# Parse YAML string
- set_fact:
    config: "{{ yaml_string | from_yaml }}"

# Generate YAML
- copy:
    content: "{{ data | to_nice_yaml }}"
    dest: /etc/myapp/config.yml

# Convert between formats
- copy:
    content: "{{ (json_string | from_json) | to_nice_yaml }}"
    dest: /tmp/converted.yml

Common Patterns

Filter JSON Array

- name: Filter servers by status
  ansible.builtin.set_fact:
    running_servers: "{{ (api_response.content | from_json) | selectattr('status', 'equalto', 'running') | list }}"

Extract Nested Values

- name: Get all IPs from JSON response
  ansible.builtin.set_fact:
    all_ips: "{{ (api_response.content | from_json).results | map(attribute='ip_address') | list }}"

Merge JSON Objects

- name: Merge default and custom config
  ansible.builtin.set_fact:
    final_config: "{{ default_config | combine(custom_config, recursive=True) }}"
  vars:
    default_config:
      log_level: info
      port: 8080
      features:
        auth: true
        cache: false
    custom_config:
      log_level: debug
      features:
        cache: true

JSON Patch (Modify and Write Back)

- name: Read existing config
  ansible.builtin.slurp:
    src: /etc/myapp/config.json
  register: config_file

- name: Parse and modify
  ansible.builtin.set_fact:
    config: "{{ config_file.content | b64decode | from_json | combine({'version': '2.0'}) }}"

- name: Write updated config
  ansible.builtin.copy:
    content: "{{ config | to_nice_json }}\n"
    dest: /etc/myapp/config.json

Troubleshooting

ErrorCauseFix
Expecting value: line 1Input isn't valid JSONVerify with echo '$var' | jq .
dict object has no attributeKey doesn't exist in parsed JSONUse default() filter: data.key | default('')
list object has no attributeJSON is array, not objectAccess with index: data[0].key
to_json outputs single quotesJinja2 native stringUse | string | to_json
Unicode escape in outputDefault encodingUse ensure_ascii=False: to_json(ensure_ascii=False)

Best Practices

  1. Use to_nice_json for config files — human-readable
  2. Use to_json for API calls — compact, efficient
  3. Handle missing keys with default() — prevent failures on optional fields
  4. Validate before parsing — check command succeeded before from_json
  5. Use uri module's built-in parsing — it auto-parses JSON responses into .json attribute
  6. Add trailing newline — {{ data | to_nice_json }}\n for POSIX files

Conclusion

Use from_json to parse command output, API responses, and file contents into Ansible dictionaries. Use to_json or to_nice_json to generate JSON for API calls and config files. Combine with selectattr, map, and combine filters for powerful data transformations. Always handle missing keys with the default() filter.