Modifying nested json conditionally using jq - json

I would like to modify something like the following JSON:
{
"foobar": {
"a": {
"adkjfe": {
"A": 1,
"foo": "bar"
}
},
"b": {
"ekjaei": {
"A": 2,
"bar": "foo"
}
}
}
}
to add more data say {"baz": ["bing", "bop"]} to the parent of A if A=1. Assuming I don't know the parent key while leaving the rest of the json untouched. I've tried many different things including:
.foobar | .. | .. | .[] | if select(.A==1) then . += {"baz": "bing"} else . end
which gives me an error and only my modified section.
The result, in this case, that I would like to see is:
{
"foobar": {
"a": {
"adkjfe": {
"A": 1,
"foo": "bar",
"baz": ["bing", "bop"]
}
},
"b": {
"ekjaei": {
"A": 2,
"bar": "foo"
}
}
}
}

Select only fields that are of type object and that match your condition (A == 1) :
jq '(.foobar | .. | select(type == "object" and .A == 1)) |= .+ {"baz": ["bing", "bop"]}' test.json
The () around the filter query take care that the whole document will be returned with the updated fields and not just your subdocument

In general it is better to avoid .. if possible, for reasons of efficiency. In the present case, the following will do the job:
(.foobar[][] | select(.A == 1)) |= .+ {"baz":["bing", "bop"]}

Related

How do I use jq to filter an array that contains both objects and strings?

So I have this input to jq:
[
"foo",
{
"a": 1,
"b": 2
},
{
"a": 1,
"b": 3
},
{
"a": 2,
"b": 2
}
]
and I want to select all objects where b is 2 ideally as an array:
[
{
"a": 1,
"b": 2
},
{
"a": 2,
"b": 2
}
]
But the string in the list makes that difficult.
If I try:
.[]| select(.b == 2)
Then I get the error:
jq: error (at /tmp/data.json:14): Cannot index string with string "b"
Any help?
Other answers have suggested using ? which is very good. Another way is to use the built-in objects filter which discards an input if it is not an object:
map(objects | select(.b == 2))
# ^^^^^^^ ^^^^^^^^^^^^^^^
# A B
A: filter out non-objects
B: At this point, we're dealing with objects
Slightly more verbose and perhaps less efficient?
There are a few options.
Ignore errors using ?:
map(select(.b? == 2))
docs, jqplay
Check the value's type in your select filter:
map(select(type == "object" and .b == 2))
docs, jqplay
Filter out non-objects:
map(objects | select(.b == 2))
docs, jqplay
Just as #hobbs suggested, use a ? to skip entries that doesn't have a .b.
As of the second question, I'd use map() so the result is an array as requested:
map(select(.b? == 2))
[
{
"a": 1,
"b": 2
},
{
"a": 2,
"b": 2
}
]
Try it online!
You can just add a ?: .[]| select(.b? == 2).
From the docs:
Optional Object Identifier-Index: .foo?
Just like .foo, but does not output even an error when . is not an array or an object.

select all keys with given value

With jq, how do I select all objects, which may be nested, with a desired value?
For example, given the following:
{
"a": "b",
"c": {
"d": {
"e": "f",
"z": "b"
}
}
}
How do I filter down to objects whose value is "b"?
{
"a": "b",
"c": {
"d": {
"z": "b"
}
}
}
Conversely, how do I select objects whose value is not "b"?
{
"c": {
"d": {
"e": "f"
}
}
}
I've attempted at the problem by using the select and walk functions, but could not get exactly what I wanted.
For the first question:
reduce paths(. == "b") as $p ({}; setpath($p; "b"))
For the second:
reduce paths(. == "b") as $p (.; delpaths([$p]))
or even more succinctly:
delpaths( [ paths(. == "b") ] )
Furthermore
The first question can also be answered without reduce, but to get it right generically requires more verbosity, e.g.:
delpaths( [ paths( (. !="b") and (type|IN("object","array") | not) ) ] )

jq: sort object values

I want to sort this data structure by the object keys (easy with -S and sort the object values (the arrays) by the 'foo' property.
I can sort them with
jq -S '
. as $in
| keys[]
| . as $k
| $in[$k] | sort_by(.foo)
' < test.json
... but that loses the keys.
I've tried variations of adding | { "\($k)": . }, but then I end up with a list of objects instead of one object. I also tried variations of adding to $in (same problem) or using $in = $in * { ... }, but that gives me syntax errors.
The one solution I did find was to just have the separate objects and then pipe it into jq -s add, but ... I really wanted it to work the other way. :-)
Test data below:
{
"": [
{ "foo": "d" },
{ "foo": "g" },
{ "foo": "f" }
],
"c": [
{ "foo": "abc" },
{ "foo": "def" }
],
"e": [
{ "foo": "xyz" },
{ "foo": "def" }
],
"ab": [
{ "foo": "def" },
{ "foo": "abc" }
]
}
Maybe this?
jq -S '.[] |= sort_by(.foo)'
Output
{
"": [
{
"foo": "d"
},
{
"foo": "f"
},
{
"foo": "g"
}
],
"ab": [
{
"foo": "abc"
},
{
"foo": "def"
}
],
"c": [
{
"foo": "abc"
},
{
"foo": "def"
}
],
"e": [
{
"foo": "def"
},
{
"foo": "xyz"
}
]
}
#user197693 had a great answer. A suggestion I got in a private message elsewhere was to use
jq -S 'with_entries(.value |= sort_by(.foo))'
If for some reason using the -S command-line option is not a satisfactory option, you can also perform the by-key sort using the to_entries | sort_by(.key) | from_entries idiom. So a complete solution to the problem would be:
.[] |= sort_by(.foo)
| to_entries | sort_by(.key) | from_entries

Parse JSON and JSON values with jq

I have an API that returns JSON - big blocks of it. Some of the key value pairs have more blocks of JSON as the value associated with a key. jq does a great job of parsing the main JSON levels. But I can't find a way to get it to 'recurse' into the values associated with the keys and pretty print them as well.
Here is the start of one of the JSON returns. Note it is only a small percent of the full return:
{
"code": 200,
"status": "OK",
"data": {
"PlayFabId": "xxxxxxx",
"InfoResultPayload": {
"AccountInfo": {
"PlayFabId": "xxxxxxxx",
"Created": "2018-03-22T19:23:29.018Z",
"TitleInfo": {
"Origination": "IOS",
"Created": "2018-03-22T19:23:29.033Z",
"LastLogin": "2018-03-22T19:23:29.033Z",
"FirstLogin": "2018-03-22T19:23:29.033Z",
"isBanned": false
},
"PrivateInfo": {},
"IosDeviceInfo": {
"IosDeviceId": "xxxxxxxxx"
}
},
"UserVirtualCurrency": {
"GT": 10,
"MB": 70
},
"UserVirtualCurrencyRechargeTimes": {},
"UserData": {},
"UserDataVersion": 15,
"UserReadOnlyData": {
"DataVersion": {
"Value": "6",
"LastUpdated": "2018-03-22T19:48:59.543Z",
"Permission": "Public"
},
"achievements": {
"Value": "[{\"id\":0,\"gamePack\":\"GAME.PACK.0.KK\",\"marblesAmount\":50,\"achievements\":[{\"id\":2,\"name\":\"Correct Round 4\",\"description\":\"Round 4 answered correctly\",\"maxValue\":10,\"increment\":1,\"currentValue\":3,\"valueUnit\":\"unit\",\"awardOnIncrement\":true,\"marbles\":10,\"image\":\"https://www.jamandcandy.com/kissinkuzzins/achievements/icons/sphinx\",\"SuccessKey\":[\"0_3_4_0\",\"0_5_4_0\",\"0_6_4_0\",\"0_7_4_0\",\"0_8_4_0\",\"0_9_4_0\",\"0_10_4_0\"],\"event\":\"Player_answered_round\",\"achieved\":false},{\"id\":0,\"name\":\"Complete
This was parsed using jq but as you can see when you get to the
"achievements": { "Vales": "[{\"id\":0,\"gamePack\":\"GAME.PACK.0.KK\",\"marblesAmount\":50,\
lq does no further parse the value at is also JSON.
Is there a filter I am missing to get it to parse the values as well as the higher level structure?
Is there a filter I am missing ...?
The filter you'll need is fromjson, but it should only be applied to the stringified JSON; consider therefore using |= as illustrated using your fragment:
echo '{"achievements": { "Vales": "[{\"id\":0,\"gamePack\":\"GAME.PACK.0.KK\",\"marblesAmount\":50}]"}}' |
jq '.achievements.Vales |= fromjson'
{
"achievements": {
"Vales": [
{
"id": 0,
"gamePack": "GAME.PACK.0.KK",
"marblesAmount": 50
}
]
}
}
recursively/1
If you want to apply fromjson recursively wherever possible, then recursively is your friend:
def recursively(f):
. as $in
| if type == "object" then
reduce keys[] as $key
( {}; . + { ($key): ($in[$key] | recursively(f) )} )
elif type == "array" then map( recursively(f) )
else try (f as $f | if $f == . then . else ($f | recursively(f)) end) catch $in
end;
This would be applied as follows:
recursively(fromjson)
Example
{a: ({b: "xyzzy"}) | tojson} | tojson
| recursively(fromjson)
yields:
{
"a": {
"b": "xyzzy"
}
}

Modify JSON values based on array of keys using jq

I need to change the value of a set of keys (defined in a variable) in a JSON object using jq.
As example, I have this JSON object:
{
foo: {
bar: 1,
baz: 2,
qux: 3
}
}
and the following variable:
update_keys = ["bar", "baz"]
I would like to say 'change the value of the keys in update_keys to X'.
The following works:
.foo = (.foo |
to_entries |
map(if .key == "bar" or .key == "baz"
then . + { "value":"X" }
else .
end) |
from_entries)
But instead of if .key == "bar" or .key == "baz" I am looking for a way to say if .key in update_keys, or a similar logic.
Here you go.
Filter
.foo |= with_entries( .value = if ([.key] | inside(["bar", "baz"])) then "X" else .value end )
Input
{
"foo": {
"bar": 1,
"baz": 2,
"qux": 3
}
}
Output
{
"foo": {
"bar": "X",
"baz": "X",
"qux": 3
}
}
Check out the cookbook for more recipies and techniques of jq usage:
https://github.com/stedolan/jq/wiki/Cookbook
Here's a slightly different approach using --argjson to parameterize update_keys, and index/1:
$ cat update.jq
.foo |= with_entries( . as $in
| if $update_keys | index($in.key) then .value = "X" else empty end)
$ update_keys='["bar", "baz"]'
$ jq --argjson update_keys "$update_keys" -f update.jq input.json
Output:
{
"foo": {
"bar": "X",
"baz": "X"
}
}
In this problem since $update_keys is just an array all that is needed is
.foo[ $update_keys[] ] = "X"
e.g. if
["bar","baz"] as $update_keys
| .foo[ $update_keys[] ] = "X"
is in filter.jq and data.json contains the (slighty corrected) data
{
"foo": {
"bar": 1,
"baz": 2,
"qux": 3
}
}
then
jq -M -f filter.jq data.json
produces
{
"foo": {
"bar": "X",
"baz": "X",
"qux": 3
}
}
If you want to pass in the value for update keys instead of defining it in your script you can easily use --argjson as peak's answer shows.