Optical
Aberration

JMESPath

What is it?

JMESPath is a query language for JSON. WIth it you can script searching large JSON datasets and transform the results.

Basic queries

A search can be as simple as a single field name. result would return the object attached to the result field at the top level of a JSON file. This can be specialized to return an object inside result by added a dot and then another field name eg result.object.

Queries can index arrays as follows

users[0].name

Which will return the name field of the first element of the array users. You can use a negative index to access an array from the end, so the index of -1 will give you the last element of an array.

Projection queries

Projections allow you to apply a query to multiple objects. The left hand side of a projection query defines the projection and the right hand side defines the query to apply to each object returned by the projection. Using the example from above by replacing the array index with a * we create a list projection users[*] which we then apply the query name to. The following returns the name field from every element in the users array.

users[*].name

There are five different types of projection:

  • list: applies query to each object in array as demonstrated above.
  • slice: applies query to each object in a slice from an array
  • flatten: applies query to each object in an array and flattens the result into a parent array
  • object: applies query to all the child objects
  • filter: applies query to a filtered version of the object

As this article isn't meant to cover the full spec of JMESPath but just give you a feel for what can be done with it I'll only cover filter projections here. A filter projection allows you to filter the left hand side of the projection before the query on the right hand side is applied to it. For example the following will return all the entries in the users array that have the field country set to USA.

users[?country=="USA"]

Functions

JMESPath has a number of standard functions that can be applied to results within a query. Using the same users example we have been using throughout we can sort the results by placing it inside the sort function.

sort(users[*].name)

You can also use functions inside filter projections. The following would return all the users whose names start with A.

users[?starts_with(name, "A")]

Transforming the results

If you want the results of your query in a specific format you can use multi-select queries. These can be used to create arrays or objects from your results. If I'm only interested in the name and age fields of the entries in my users array I can transform the list query results using a multi-select hash

users[*].{"name": name, "age": age}

Why am I telling you this

Well, AWS uses JMESPath in their SDK generation. If you have used their CLI tool you might recognise the syntax from the --query option. So far in my work on Soto (a third party Swift SDK for AWS) I have been able to hardcode the queries I came across and write custom code for them. Through recent work though, I have found a number of queries that were too complex. So I took the leap and decided to implement JMESPath for Swift. You can find the code here. Given there are versions of JMESPath for a number of languages already I had a lot of prior art to help in my implementation. The Swift version is based off the Rust version and follows its implementation fairly closely. The JMESPath site provides a comprehensive set of compliance tests and my version passes them all so I am fairly confident it is up to scratch.

The Swift code works as follows. You create a compiled Expression and then use Expression.search to apply the expression to your JSON.

let expression = try JMESExpression.compile("users[*].name")
let result = try expression.search(json: myjson)

The above will return Any as it is undefined what type the expression will return. You can pass this straight into JSONSerialization.data to get the result as JSON if desired. Or if you know the type that the search will return you can add it into the search call as follows

let result = try expression.search(json: myjson, as: [String].self)

In addition to parsing JSON I've added Mirror reflection to search objects of any type. This means the Swift implementation of JMESPath isn't restricted to just querying JSON data it can be used to query any data structure. Below is a sample where we have a struct Service which includes an array of User. We use JMESPath to create a sorted list of all the User member name.

struct Service {
    struct User {
        let name: String
    }
    let users: [User]
}
let service = Service(
    users: [
        .init(name: "james"),
        .init(name: "anne"),
        .init(name: "tom")
    ]
)
let expression = try JMESExpression.compile("users[*].name | sort(@)")
let result = try expression.search(object: service, as: [String.self])
assert(result == ["anne", "james", "tom"])

The expression used here includes two new pieces of grammar | which pipes the result of the expression on the left hand side into the expression on the right hand side and @ which indicates the current result being evaluated. So above the array of names returned by users[*].name is passed into the sort function.

Usage

JMESPath provides an easy way to script filtering and transforming of JSON responses from different services. It can be used to parse responses from cloud providers and other services from your backend into the formats that your client expects.

More Information

This article only details a small sample of the possibilities with JMESPath. If you'd like to find out more about JMESPath you can go the website jmespath.org. It has a great tutorial section where you can test out different queries.

You can checkout the Swift version of JMESPath here https://github.com/adam-fowler/jmespath.swift.