Simplify JSON Parsing with JSONata

Simplify JSON Parsing with JSONata

What is JSONata?

JSONata is a lightweight query and transformation language for JSON; it provides:

  • Parsing: Easily pull the data from your payloads with minimal syntax.
  • Built-in functions: Manipulate your data, for example, sum and join data without moving from JSONata into another language (such as Python).
  • Path Transversal: Navigate nested objects using chained, wildcard, parent, and child selectors.
  • Transformation: Generate new JSON payloads easily.
  • Python Library: Use JSONata within Python using the jsonata library.
  • Online Testing: Validate expressions in the JSONata Exerciser.

Why do I need JSONata?

JSON is the defacto for representing data (think REST APIs) and one of the main data formats used in network automation. However, the data we typically get back is nested, and to be frank, much of the time is painful to work with. For example, the multiple loops and looks ups required to obtain a piece of data from the jungle of JSON that you have been provided with.

Other tools exist, such as JMESpath, JSONpath, Dq, Jq, but they either have limited functionality, have a bloated syntax or most commonly provide limited transformation support.

For a deep dive into JSONata check out our members-only JSONata Tech Session here.

Some examples of where you could use JSONata:

  • Extracting Information: Get specific data from API responses like MTU, or admin status of interface data.
  • Transforming Data: Modify data received from a device to then provide to other frameworks (e.g., Pytest).
  • Finding Specific Data: Locate all instances of a specific attribute, such as interfaces with MTU's less than 9000.
  • Flattening Data: Simplify the data structure for easy import into Panda DataFrames.

Examples

Let's step through some examples based on the following payload. You can also follow along via the link: https://try.jsonata.org/tCiU0h8NA

{
  "TABLE_vrf": {
    "ROW_vrf": {
      "TABLE_adj": {
        "ROW_adj": [
          {
            "flags": null,
            "intf-out": "Ethernet1/1",
            "ip-addr-out": "10.1.1.2",
            "mac": "5000.0009.0000",
            "time-stamp": "PT10M32S"
          },
          {
            "flags": null,
            "intf-out": "Ethernet1/2",
            "ip-addr-out": "10.1.2.2",
            "mac": "5000.000a.0000",
            "time-stamp": "PT2M28S"
          },
          {
            "flags": null,
            "intf-out": "Ethernet1/4",
            "ip-addr-out": "10.1.4.2",
            "mac": "0205.8671.2703",
            "time-stamp": "PT9M23S"
          }
        ]
      },
      "cnt-total": "3",
      "vrf-name-out": "default"
    }
  }
}

Simple Parsing

Let's start with some simple parsing where we can select the various keys of the JSON object to walk the path.

// Walking the path using keys
TABLE_vrf.ROW_vrf.TABLE_adj.ROW_adj
# Output:
# [
#   {
#     "flags": null,
#     "intf-out": "Ethernet1/1",
#     "ip-addr-out": "10.1.1.2",
#     "mac": "5000.0009.0000",
#     "time-stamp": "PT10M32S"
#   },
#   {
#     "flags": null,
#     "intf-out": "Ethernet1/2",
#     "ip-addr-out": "10.1.2.2",
#     "mac": "5000.000a.0000",
#     "time-stamp": "PT2M28S"
#   },
#   {
#     "flags": null,
#     "intf-out": "Ethernet1/4",
#     "ip-addr-out": "10.1.4.2",
#     "mac": "0205.8671.2703",
#     "time-stamp": "PT9M23S"
#   }
# ]
// Select a single element from the array
TABLE_vrf.ROW_vrf.TABLE_adj.ROW_adj[0]
# Output:
# {
#   "flags": null,
#   "intf-out": "Ethernet1/1",
#   "ip-addr-out": "10.1.1.2",
#   "mac": "5000.0009.0000",
#   "time-stamp": "PT10M32S"
# }

Transversing Levels

To ease the process of walking a nested path, we can transverse multiple levels like so. Here we use ** to traverse multiple levels to parse the values for the key ip-addr-out.

**.'ip-addr-out'
# Output:
# [
#   "10.1.1.2",
#   "10.1.2.2",
#   "10.1.4.2"
# ]

Comparisons

We can also perform comparisons using various operators. Here is an example: we combine the equals operator with our previous multi-level transverse to select data with a mac of 5000.000a.0000.

**[mac='5000.000a.0000']
# Output:
# {
#  "flags": null,
#  "intf-out": "Ethernet1/2",
#  "ip-addr-out": "10.1.2.2",
#  "mac": "5000.000a.0000",
#  "time-stamp": "PT2M28S"
# }

Transformations

When it comes to transformations, you can place your expressions into the open and closing brackets, along with your custom keys. Like so:

{
   "ip_addr_lookup": **[mac=”5000.000a.0000”].”ip-addr-out”',
   "all_macs": **.mac
}
# Output:
# {
#   "ip_addr_lookup": "10.1.2.2",
#   "all_macs": [
#     "5000.0009.0000",
#     "5000.000a.0000",
#     "0205.8671.2703"
#   ]
# } 

JSONata with Python

JSONata also provides a Python library that you can use - jsonata.

To install jsonata you perform a:

$ pip install jsonata 

# or

$ poetry add jsonata

Let's now show our transformation example using the Python library.

Note: I've also installed rich to enhance the terminal output when printing.

import jsonata
import json
from rich import print as rprint

jncontext = jsonata.Context()

data = """{
    "TABLE_vrf": {
        "ROW_vrf": {
            "vrf-name-out": "default",
            "cnt-total": "6",
            "TABLE_adj": {
                "ROW_adj": [
                    {
                        "intf-out": "Ethernet1/1",
                        "ip-addr-out": "10.1.1.2",
                        "time-stamp": "PT10M32S",
                        "mac": "5000.0009.0000",
                        "flags": null
                    },
                    {
                        "intf-out": "Ethernet1/2",
                        "ip-addr-out": "10.1.2.2",
                        "time-stamp": "PT2M28S",
                        "mac": "5000.000a.0000",
                        "flags": null
                    },
                    {
                        "intf-out": "Ethernet1/4",
                        "ip-addr-out": "10.1.4.2",
                        "time-stamp": "PT9M23S",
                        "mac": "0205.8671.2703",
                        "flags": null
                    }
                ]
            }
        }
    }
}"""

# Parse the data string into a JSON object
json_data = json.loads(data)

# JSONata expression
jsonata_expr = """{
    "ip_addr_lookup": **[mac="5000.000a.0000"]."ip-addr-out",
    "all_macs": **.mac
}""".replace(
    "\n", ""
)

# Taking the jsonata expression and the input data and returning the result.
result = jncontext(jsonata_expr, json_data)

# Printing the result
rprint(result)
# Output:
# {'ip_addr_lookup': '10.1.2.2', 'all_macs': ['5000.0009.0000', '5000.000a.0000', '0205.8671.2703']}

That wraps up this post. I hope you`ve enjoyed it, and it saves you some of the headaches when dealing with nested data when automating your network.

Until next time …

Subscribe to our newsletter and stay updated.

Don't miss anything. Get all the latest posts delivered straight to your inbox.
Great! Check your inbox and click the link to confirm your subscription.
Error! Please enter a valid email address!