Filter object by property and select with key in jmespath

21.4k views Asked by At

I'm trying to filter properties of an object in jmespath based on the value of a subproperty and want to include only those properties where the subproperty is set to a specific value.

Based on this example data:

{
  "a": {
    "feature": {
      "enabled": true,
    }
  },
  "b": {
  },
  "c": {
    "feature": {
      "enabled": false
     }
  }
}

I'd like to get an object with all properties where the feature is enabled.

{
  "a": {
    "feature": {
      "enabled": true,
    }
  }
}

I figured I could use this jmespath query to filter the objects where property. enabled is set to true. Unfortunateley, it doesn't seem to work and instead returns an empty array.

*[?feature.enabled==`true`]

*.feature.enabled or *[feature.enabled] return just the boolean values without any context.

Even if *[?feature.enabled==true] would work, it would just be an array of the property values, but I need the keys (a and c) aswell. Is there any way to make this happen in jmespath?

This is all part of an ansible playbook, so there would certainly be a way to achieve selection in a different way (Jinja2 templates or custom plugin) but I wanted to try jmespath and would reason, that it should be capable of such a task.

4

There are 4 answers

3
Konstantin Suvorov On BEST ANSWER

Sorry, but AFAIK this is impossible in native JMESPath.
There are custom built-in functions for this purpose in different tools like to_entries in jq.
For jmespath.py and thus for Ansible there is hanging pull request to implement keys manipulation.

Update: I've made a patched version of json_query filter.
See this answer for additional info.

4
dreftymac On

Short answer (TL;DR)

  • Actually, yes, this is possible with nothing more than native jmespath
  • The problem is, queries against the source dataset will be extremely cumbersome, because the source dataset is poorly normalized for this kind of general-purpose jmespath query.

Example

The following (way-too-long) jmespath query against the source data in the OP...

[
  {
      "item_key":           `a`
      ,"feature_enabled":   @.a.feature.enabled
      ,source_object:       @.a
  }
  ,{
      "item_key":           `b`
      ,"feature_enabled":   @.b.feature.enabled
      ,source_object:       @.b
  }
  ,{
      "item_key":           `c`
      ,"feature_enabled":   @.c.feature.enabled
      ,source_object:       @.c
  }
]|[? feature_enabled == `true`]

... produces the following result

[
  {
    "item_key": "a",
    "feature_enabled": true,
    "source_object": {
      "feature": {
        "enabled": true
      }
    }
  }
]

Which is identical or substantially similar to the desired output, but the fact that we had to bend our brain to get there suggests we are trying to force a square peg through a round hole.

Pitfalls

The reason this jmespath query looks so long and cumbersome is that the source dataset itself is poorly normalized for a general purpose jmespath query.

That is because it uses object keys as a top-level collation method, when a sequentially-indexed-list would have sufficed.

Whenever you have a dataset that can potentially contain an arbitrary number of values it is almost always preferable to use a sequence for top-level collation, instead of object keys.

If you find you can do something in jmespath, but you have to modify your jmespath query whenever you add another "entry" to your "set of entries of arbitrary (non-fixed) length" you are fighting against Jmespath instead of working with it.

Whenever you see a query that seems "impossible to accomplish" with Jmespath, you are almost certainly dealing with a data structure that is using objects where sequences may have been more suitable.

Object keys usually mean a fixed number of properties, which jmespath can handle just fine.

Even object properties of arbitrarily deep nesting are just fine, so long as those object properties are not being used as a substitute for sequential enumeration.

Things only start to get uncomfortable when you find you are having to create sequences-of-objects in order to get around objects-of-objects ... which is entirely doable in jmespath, but it is going to be painful.

See also

1
techraf On

With dict2items filter in Ansible 2.5 and later, you can do it with:

- debug:
    msg: "{{ dict(my_data | dict2items | json_query('[?value.feature.enabled].[key, value]')) }}"

The result:

"msg": {
    "a": {
        "feature": {
            "enabled": true
        }
    }
}
3
user9642565 On

You can use:

<json_data>|[*]|[? @.feature.enabled]