Juve Juve - 1 month ago 8
JSON Question

jq: Testing if a key is in a list of predefined keys

I have a case where I need to parse quoted JSON in JSON.
I know which optional attributes will contain quoted JSON and which not.
Therefore, I want to check if the attribute keys are in a list of possible keys. I already have the following:

# attributes "a" and "b" contain quoted JSON
echo '{"a":"{\"x\":1}","y":2}' |
jq -c '
def is_json($o): ["a","b"] | (map(select(. == $o)) | length) > 0;
with_entries(if is_json(.key) then .value = (.value|fromjson) else . end)
'


This already produces the desired output:
{"a":{"x":1},"y":2}
. However, the checking of the attribute name looks clumsy, given that
jq
provides a lot built-in functions such as
has
,
in
,
contains
,
inside
, etc.

Question: Is there a better way of checking if an attribute key is in a given list?

Edit: Here is the current solution, based on peak's answer.

#!/bin/bash
to_array_string() { echo "$*" | awk -v OFS='","' 'NF > 0 {$1=$1; print "\""$0"\""}'; }
to_json_array_string() { echo "["`to_array_string "$@"`"]"; }

parse_json_jq() { jq -c "
reduce keys[] as \$key
(.; if any((`to_array_string "$@"`); . == \$key) and .[\$key] != null then .[\$key] |= fromjson else . end)
";}

Answer

There are three ways in which your program can be improved:

  1. (efficiency) avoiding the creation of an unnecessary array (in is_json);
  2. (efficiency) using "short-circuit" semantics to avoid iterating unnecessarily;
  3. (efficiency) avoid the construction/deconstruction involved with with_entries;

For the most part, I think you will agree that the alternatives offered here are simpler, more concise, or more readable.

If you have version 1.5 of jq or later, the main improvements can be had using any/2:

def is_json($o): any( ("a","b"); . == $o );

with_entries(if is_json(.key) then .value |= fromjson else . end)

Notice also the use of '|=' where you had used '='.

If your jq does not have any/2, then you could use the following definition, though it lacks short-circuit semantics:

def any(s): reduce s as $i (false; . == true or $i);

Finally, to avoid using with_entries, you could use reduce and eliminate is_json entirely:

reduce keys[] as $key
  (.; if any(("a","b"); . == $key) then .[$key] |= fromjson else . end)