How can I get a list of specific key values from an array of objects in JSON?

Posted by Nick Anderson
July 27, 2023

This question was covered in The agent is in, Episode 27 - CFEngine Q&A: Policy questions.

Given the following JSON, how can I get a list containing just the values of name?

[
  { "name": "Aurora", "description": "Illuminating" },
  { "name": "Orion", "description": "Stellar" },
  { "name": "Luna", "description": "Serene" },
  { "name": "Phoenix", "description": "Resilient" },
  { "name": "Atlas", "description": "Strong" }
]

Using maparray()

The most concise and direct way to achieve something like this is to use the maparray() function. It iterates over a list or data container applying a pattern based on $(this.k) and $(this.v) of the currently iterated element to produce a list.

bundle agent __main__
{
  methods:
    "example";
}
bundle agent example
{
  vars:
    "d" data => '[
  { "name": "Aurora",  "description": "Illuminating" },
  { "name": "Orion",   "description": "Stellar" },
  { "name": "Luna",    "description": "Serene" },
  { "name": "Phoenix", "description": "Resilient" },
  { "name": "Atlas",   "description": "Strong" }
]';

    "names"
      slist => sort(
        maparray( "$(d[$(this.k)][name])", d ),
        lex
      );
}

Here we run with --show-evaluated-vars=default:example to show variable values at the end of the agent run filtered to show only variables in the default namespace from bundles matching example and --file /tmp/example-1.cf to specify the policy entry:

command
cf-agent --show-evaluated-vars=default:example --file /tmp/example-1.cf
output
Variable name                            Variable value                                               Meta tags                                Comment
default:example.d                        [{"description":"Illuminating","name":"Aurora"},{"description":"Stellar","name":"Orion"},{"description":"Serene","name":"Luna"},{"description":"Resilient","name":"Phoenix"},{"description":"Strong","name":"Atlas"}] source=promise
default:example.names                     {"Atlas","Aurora","Luna","Orion","Phoenix"}                 source=promise

Using an associative array

Associative arrays are a very useful and flexible construct. They allow you to build structured data up key value by key value, potentially applying specific rules at each step.

We need to iterate over the data container and extract the names prior to getting the consolidated list.

bundle agent __main__
{
  methods:
    "example";
}
bundle agent example
{
  vars:
    "d"
      data => '[
  { "name": "Aurora",  "description": "Illuminating" },
  { "name": "Orion",   "description": "Stellar" },
  { "name": "Luna",    "description": "Serene" },
  { "name": "Phoenix", "description": "Resilient" },
  { "name": "Atlas",   "description": "Strong" }
]';

    "d_keys"
      slist => getindices( d );

    "name[$(d_keys)]"
      string => "$(d[$(d_keys)][name])";

    "names"
      slist => sort( getvalues( name ), lex );
}

Again, we run the agent with --show-evaluated-vars=default:example to see these veriables:

command
cf-agent --show-evaluated-vars=default:example --file /tmp/example-2.cf
output
Variable name                            Variable value                                               Meta tags                                Comment
default:example.d                        [{"description":"Illuminating","name":"Aurora"},{"description":"Stellar","name":"Orion"},{"description":"Serene","name":"Luna"},{"description":"Resilient","name":"Phoenix"},{"description":"Strong","name":"Atlas"}] source=promise
default:example.d_keys                    {"0","1","2","3","4"}                                       source=promise
default:example.name[0]                  Aurora                                                       source=promise
default:example.name[1]                  Orion                                                        source=promise
default:example.name[2]                  Luna                                                         source=promise
default:example.name[3]                  Phoenix                                                      source=promise
default:example.name[4]                  Atlas                                                        source=promise
default:example.names                     {"Atlas","Aurora","Luna","Orion","Phoenix"}                 source=promise

In the above output you may wonder why default:example.d_keys is a list of numbers. That’s because getindices() returns the positions of each element when targeting a list, data container array, or data container object of anonymous objects.

Let’s take it a bit further, this time getting the list of names where description begins with S.

bundle agent __main__
{
  methods:
    "example";
}
bundle agent example
{
  vars:
    "d"
      data => '[
  { "name": "Aurora",  "description": "Illuminating" },
  { "name": "Orion",   "description": "Stellar" },
  { "name": "Luna",    "description": "Serene" },
  { "name": "Phoenix", "description": "Resilient" },
  { "name": "Atlas",   "description": "Strong" }
]';

  "d_keys"
    slist => getindices( d );

  "name[$(d_keys)]"
    string => "$(d[$(d_keys)][name])",
    if => regcmp( "S.*", "$(d[$(d_keys)][description])" );

  "names"
    slist => sort( getvalues( name ), lex );
}

And look at the result:

command
cf-agent --show-evaluated-vars=default:example --file /tmp/example-3.cf
output
Variable name                            Variable value                                               Meta tags                                Comment
default:example.d                        [{"description":"Illuminating","name":"Aurora"},{"description":"Stellar","name":"Orion"},{"description":"Serene","name":"Luna"},{"description":"Resilient","name":"Phoenix"},{"description":"Strong","name":"Atlas"}] source=promise
default:example.d_keys                    {"0","1","2","3","4"}                                       source=promise
default:example.name[1]                  Orion                                                        source=promise
default:example.name[2]                  Luna                                                         source=promise
default:example.name[4]                  Atlas                                                        source=promise
default:example.names                     {"Atlas","Luna","Orion"}                                    source=promise

We can even do something like uppercase names where descriptions starting with S have more than 6 characters and lowercase names where descriptions starting with S have less than 7 characters.

bundle agent __main__
{
  methods:
    "example";
}
bundle agent example
{
  vars:
    "d"
      data => '[
  { "name": "Aurora",  "description": "Illuminating" },
  { "name": "Orion",   "description": "Stellar" },
  { "name": "Luna",    "description": "Serene" },
  { "name": "Phoenix", "description": "Resilient" },
  { "name": "Atlas",   "description": "Strong" }
]';

    "d_keys"
      slist => getindices( d );

    "name[$(d_keys)]"
      string => string_upcase( "$(d[$(d_keys)][name])" ),
      if => and(
        regcmp( "S.*", "$(d[$(d_keys)][description])" ),
        isgreaterthan(
          string_length( "$(d[$(d_keys)][description])" ),
          6));

    "name[$(d_keys)]"
      string => string_downcase( "$(d[$(d_keys)][name])" ),
      if => and(
        regcmp( "S.*", "$(d[$(d_keys)][description])" ),
        islessthan(
          string_length( "$(d[$(d_keys)][description])" ),
          7));

    "names"
      slist => sort( getvalues( name ), lex );
}

Output:

command
cf-agent --no-lock --show-evaluated-vars=default:example --file /tmp/example-4.cf
output
Variable name                            Variable value                                               Meta tags                                Comment
default:example.d                        [{"description":"Illuminating","name":"Aurora"},{"description":"Stellar","name":"Orion"},{"description":"Serene","name":"Luna"},{"description":"Resilient","name":"Phoenix"},{"description":"Strong","name":"Atlas"}] source=promise
default:example.d_keys                    {"0","1","2","3","4"}                                       source=promise
default:example.name[1]                  ORION                                                        source=promise
default:example.name[2]                  luna                                                         source=promise
default:example.name[4]                  atlas                                                        source=promise
default:example.names                     {"ORION","atlas","luna"}                                    source=promise