Replace JSON objects in unknown positions inside a JSON tree - json

In a JSON file I need to replace encrypted values with their clear text values as an initialization process using the command line tool jq. An application will then re-encrypt the values with its own keys, overwriting the clear text values. Encrypted values are represented as "$crypto" objects, containing information about the encryption method and which keys were used, looking like this:
{
"$crypto" : {
"type" : "x-simple-encryption",
"value" : {
"cipher" : "AES/CBC/PKCS5Padding",
"stableId" : "someId",
"salt" : "4J5ckE6+JaS8TLqAN4073g==",
"data" : "vBeHAPJXLl+X/8Enp9vxMA==",
"keySize" : 16,
"purpose" : "someDescription",
"iv" : "N2xCe5RiJibHv9hLY+OduA==",
"mac" : "VoOo1BKptwfqIJeSOb/qGA=="
}
}
}
These "$crypto" objects can be anywhere in the JSON structure. A sample input document looks like this:
{
"unknownKey1" : {
"unknownKey2" : {
"name" : "JWT_SESSION",
"properties" : {
"maxTokenLifeMinutes" : 120,
"tokenIdleTimeMinutes" : 30
}
},
"unknownKey3" : [
{
"unknownKey4" : "STATIC_USER",
"unknownKey5" : {
"unknownKey6" : "internal/user",
"unknownKey7" : "anonymous",
"unknownKey8" : {
"$crypto" : {
"type" : "x-simple-encryption",
"value" : {
"cipher" : "AES/CBC/PKCS5Padding",
"stableId" : "someId",
"salt" : "4J5ckE6+JaS8TLqAN4073g==",
"data" : "vBeHAPJXLl+X/8Enp9vxMA==",
"keySize" : 16,
"purpose" : "someDescription",
"iv" : "N2xCe5RiJibHv9hLY+OduA==",
"mac" : "VoOo1BKptwfqIJeSOb/qGA=="
}
}
}
},
"enabled" : true
}
]
}
}
So the value of "unknownKey8" was encrypted. I need that document to look like this:
{
"unknownKey1" : {
"unknownKey2" : {
"name" : "JWT_SESSION",
"properties" : {
"maxTokenLifeMinutes" : 120,
"tokenIdleTimeMinutes" : 30
}
},
"unknownKey3" : [
{
"unknownKey4" : "STATIC_USER",
"unknownKey5" : {
"unknownKey6" : "internal/user",
"unknownKey7" : "anonymous",
"unknownKey8" : "clearTextValue"
},
"enabled" : true
}
]
}
}
I have been able to find crypto objects in the input file using the following command:
cat input.json | jq 'paths | select(.[-1] == "$crypto")'
[
"unknownKey1",
"unknownKey3",
0,
"unknownKey5",
"unknownKey8",
"$crypto"
]
But I have not been able to make meaningful progress on performing the replacement.

The following performs the replacement described in the text. An invocation along the lines of
jq --arg cleartext "clearTextValue" -f decrypt.jq sample.json
is assumed. If your jq does not have walk/1, then either upgrade to jq 1.6 or include its def before its invocation (google search terms: jq def walk builtin.jq).
# input is assumed to be an object
def decrypt($value):
with_entries(if .value|type == "object"
then with_entries(if .value | (type == "object" and has("$crypto"))
then .value = $value else . end)
else . end) ;
walk(if type == "object" then decrypt($cleartext) else . end)

As an addition to the accepted answer and if using jq v1.5, use this as decrypt.jq
# input is assumed to be an object
def decrypt($value):
with_entries(if .value|type == "object"
then with_entries(if .value | (type == "object" and has("$crypto"))
then .value = $value else . end)
else . end) ;
# walk was added after the release of jq#1.5
def walk(f):
. as $in
| if type == "object" then
reduce keys[] as $key
( {}; . + { ($key): ($in[$key] | walk(f)) } ) | f
elif type == "array" then map( walk(f) ) | f
else f
end;
walk(if type == "object" then decrypt($cleartext) else . end)

Related

jq: create new json basing on existing json with usage if else

now I have following problem.
there is such input json
{
“inn” : “qwerty”,
“cuid” : “1234”,
“type” : "legal",
“reqId” : “asdfg”
}
It should be transformed into
{
“inn” : “qwerty”,
“reqId” : “asdfg”
}
or into
{
“cuid” : “1234”,
“reqId” : “asdfg”
}
it depends on a value of "type" field. How can I do that with JQ?
My attempts were failed, it all was about trying such way
.inn as $inn | .cuid as $cuid | {if .type == "person" then $inn else $cuid end}
but it gave such result
jq: error: syntax error, unexpected FIELD, expecting ':' (Unix shell quoting issues?) at , line 1:
.inn as $inn | .cuid as $cuid | {if .type == "person" then $inn else $cuid end}
jq: 1 compile error
exit status 3
To get the following filter where:
.type == "person" --> We use keys: inn and reqId
.type != "person" --> We use keys: cuid and reqId
We can use:
if .type == "person" then { inn } else { cuid } end + { reqId }
The differences I've made:
Added the last part: + { reqId }. Here we add the reqId to the object created by the if statement.
I've removed the .inn as $inn part this we can just use the key for this.
JqPlay Demo
You can solve your problem easily with one if then else end expression:
jq 'if .type == "person"
then {inn, reqId}
else {cuid, reqId}
end' <<< '{ "inn" : "qwerty", "cuid" : "1234", "type" : "legal", "reqId" : "asdfg" }'
# result
# {
# "cuid": "1234",
# "reqId": "asdfg"
# }
jq 'if .type == "person"
then {inn, reqId}
else {cuid, reqId}
end' <<< '{ "inn" : "qwerty", "cuid" : "1234", "type" : "person", "reqId" : "asdfg" }'
# result
# {
# "inn": "qwerty",
# "reqId": "asdfg"
# }

How to modify (i.e. change part of, but keep the rest of JSON the same) a property in an array of objects using jq

Note, I only want to change part of the JSON data in a large JSON blob. Deep inside the JSON blob, there is an array of objects. These objects have different properties, but I can identify the object I want to change by it's title. I'd like to use jq to do this.
Here's an example JSON:
{
"propA" : "valA",
"anArrayOfObjs" : [
{
"title" : "blah",
"objA" : {
"objB" : {
"propA" : "valA",
"propB" : "valB"
}
}
},
{
"title" : "welcome",
"objA" : {
"objB" : {
"propA" : "valA",
"propC" : "valC"
}
}
},
{
"objA" : {
"objB" : {
"propA" : "valA",
"propD" : "valD"
}
}
}
]
}
I want to change the value of objA.objB.propA only for the object in anArrayOfObjs where title == "welcome".
thanks for any tips you can give.
.path.to.anArrayOfObjs |= map(
select(.title == "welcome").objA.objB.propA = "new value"
)
To modify multiple properties:
.path.to.anArrayOfObjs |= map(
select(.title == "welcome").objA.objB |= (
.propA = "new value" | .propC = "new value"
)
)

How to substitute strings in JSON with jq based on the input

Given the input in this form
[
{
"DIR" : "/foo/bar/a/b/c",
"OUT" : "/foo/bar/x/y/z",
"ARG" : [ "aaa", "bbb", "/foo/bar/a", "BASE=/foo/bar" ]
},
{
"DIR" : "/foo/baz/d/e/f",
"OUT" : "/foo/baz/x/y/z",
"ARG" : [ "ccc", "ddd", "/foo/baz/b", "BASE=/foo/baz" ]
},
{
"foo" : "bar"
}
]
I'm trying to find out how to make jq transform that into this:
[
{
"DIR" : "BASE/a/b/c",
"OUT" : "BASE/x/y/z",
"ARG" : [ "aaa", "bbb", "BASE/a", "BASE=/foo/bar" ]
},
{
"DIR" : "BASE/d/e/f",
"OUT" : "BASE/x/y/z",
"ARG" : [ "ccc", "ddd", "BASE/b", "BASE=/foo/baz" ]
},
{
"foo" : "bar"
}
]
In other words, objects having an "ARG" array, containing a string that starts with "BASE=" should use the string after "BASE=", e.g. "/foo" to substitute other string values that start with "/foo" (except the "BASE=/foo" which should remain unchanged")
I'm not even close to finding a solution myself, and at this point I'm unsure that jq alone will do the job.
With jq:
#!/usr/bin/jq -f
# fix-base.jq
def fix_base:
(.ARG[] | select(startswith("BASE=")) | split("=")[1]) as $base
| .DIR?|="BASE"+ltrimstr($base)
| .OUT?|="BASE"+ltrimstr($base)
| .ARG|=map(if startswith($base) then "BASE"+ltrimstr($base) else . end)
;
map(if .ARG? then fix_base else . end)
You can run it like this:
jq -f fix-base.jq input.json
or make it an executable like this:
chmod +x fix-base.jq
./fix-base.jq input.json
Don't worry, jq alone will do the job:
jq 'def sub_base($base): if (startswith("BASE") | not) then sub($base; "BASE") else . end;
map(if .["ARG"] then ((.ARG[] | select(startswith("BASE=")) | split("=")[1]) as $base
| to_entries
| map(if (.value | type == "string") then .value |= sub_base($base)
else .value |= map(sub_base($base)) end)
| from_entries)
else . end)' input.json
The output:
[
{
"DIR": "BASE/a/b/c",
"OUT": "BASE/x/y/z",
"ARG": [
"aaa",
"bbb",
"BASE/a",
"BASE=/foo/bar"
]
},
{
"DIR": "BASE/d/e/f",
"OUT": "BASE/x/y/z",
"ARG": [
"ccc",
"ddd",
"BASE/b",
"BASE=/foo/baz"
]
},
{
"foo": "bar"
}
]
Some helper functions make the going much easier. The first is generic and worthy perhaps of your standard library:
# Returns the integer index, $i, corresponding to the first element
# at which f is truthy, else null
def indexof(f):
label $out
| foreach .[] as $x (null; .+1;
if ($x|f) then (.-1, break $out) else empty end) // null;
# Change the string $base to BASE using gsub
def munge($base):
if type == "string" and (test("^BASE=")|not) then gsub($base; "BASE")
elif type=="array" then map(munge($base))
elif type=="object" then map_values(munge($base))
else .
end;
And now the easy part:
map(if has("ARG")
then (.ARG|indexof(test("^BASE="))) as $ix
| if $ix
then (.ARG[$ix]|sub("^BASE=";"")) as $base | munge($base)
else . end
else . end )
Some points to note:
You may wish to use sub rather than gsub in munge;
The above solution assumes you want to make the change in all keys, not just "DIR", "OUT", and "ARG"
The above solution allows specifications of BASE that include one or more occurrences of "=".

jq: easiest way to recursively remove objects based on object value condition

I would like to use jq to remove all dictionaries within a JSON "object" (I used that term generally to refer to either an Array or a Dictionary) that
a) contain a key named "delete_me", AND
b) where the key "delete_me" meets some predetermined condition (null, non-zero, true, etc)
Basically, the logic I want to implement is: walk the input, and at each node, if that node is not an Array or an Object, then keep it and move on, otherwise, keep it but remove from it any children that are dictionaries for which either condition a) or b) fail.
Any suggestions?
Sample input:
{
"a": { "foo": "bar" },
"b": {
"i": {
"A": {
"i": [
{
"foo": {},
"bar": {
"delete_if_this_is_null": false,
"an_array": [],
"another_array": [
{
"delete_if_this_is_null": null,
"foo": "bar"
}
],
"etc": ""
},
"foo2": "s"
},
{
"foo": {
"an_array": [
{
"delete_if_this_is_null": "ok",
"foo":"bar",
"another_object": { "a":1 }
},
{
"delete_if_this_is_null": null,
"foo2":"bar2",
"another_object": { "a":1 },
"name": null
}
],
"an_object": {
"delete_if_this_is_null":null,
"foo3":"bar3"
}
},
"zero": 0,
"b": "b"
}
]
}
}
}
}
should yield, if the "delete_me" key is delete_if_this_is_null and the predetermined condition is delete_if_this_is_null == null:
{
"a": { "foo": "bar" },
"b": {
"i": {
"A": {
"i": [
{
"foo": {},
"bar": {
"delete_if_this_is_null": false,
"an_array": [],
"another_array": [],
"etc": ""
},
"foo2": "s"
},
{
"foo": {
"an_array": [
{
"delete_if_this_is_null": "ok",
"foo":"bar",
"another_object": { "a":1 }
}
]
},
"zero": 0,
"b": "b"
}
]
}
}
}
}
UPDATE: Here's the solution: Assume the input is in a file 'input.json':
jq 'def walk(f):
. as $in
| if type == "object" then
reduce keys[] as $key
( {}; . + { ($key): ($in[$key] | walk(f)) } ) | f
elif type == "array" then map( walk(f) ) | f
else f
end;
def mapper(f):
if type == "array" then map(f)
elif type == "object" then
. as $in
| reduce keys[] as $key
({};
[$in[$key] | f ] as $value
| if $value | length == 0 then .
else . + {($key): $value[0]} end)
else .
end;
walk( mapper(select((type == "object" and .delete_if_this_is_null == null) | not)) )' < input.json
Jeff's solution may zap too much. For example, using:
def data: [1,2, {"hello": {"delete_me": true, "a":3 }, "there": 4} ]; ];
Jeff's solution yields empty (i.e. nothing).
The following may therefore be closer to what you're looking for:
walk(if (type == "object" and .delete_me) then del(.) else . end )
For data, this yields:
[1,2,{"hello":null,"there":4}]
Alternative Solution
If a solution that eliminates the "hello":null in the above example is required, then a variant of jq's map_values/1 is needed. Here's one approach:
def mapper(f):
if type == "array" then map(f)
elif type == "object" then
. as $in
| reduce keys[] as $key
({};
[$in[$key] | f ] as $value
| if $value | length == 0 then .
else . + {($key): $value[0]} end)
else .
end;
data | walk( mapper(select((type == "object" and .delete_me) | not)) )
The result is:
[1,2,{"there":4}]
Here is a solution which uses a recursive function:
def clean(condition):
if type == "object" then
if condition
then empty
else
with_entries(
if (.value|type) == "object" and (.value|condition)
then empty
else .value |= clean(condition)
end
)
end
elif type == "array" then
map(
if type == "object" and condition
then empty
else clean(condition)
end
)
else .
end
;
clean(
has("delete_if_this_is_null") and (.delete_if_this_is_null == null)
)
I'm not sure what exactly you're trying to accomplish in your question but I'm assuming you want to recursively search through a json response and remove json objects that satisfy some condition.
You can do this rather easily with the help of the walk filter that will be coming up in a future version of jq, see the implementation in the source.
# Apply f to composite entities recursively, and to atoms
def walk(f):
. as $in
| if type == "object" then
reduce keys[] as $key
( {}; . + { ($key): ($in[$key] | walk(f)) } ) | f
elif type == "array" then map( walk(f) ) | f
else f
end;
With that, you can filter them out like so:
def filter_objects(predicate): # removes objects that satisfies some predicate
walk(
if (type == "object") and (predicate) then
empty
else
.
end
)
;
filter_objects(.delete_me) # remove objects that has a truthy property "delete_me"

jq Filter on sub object value

I have a json file people.json:
{
"Joe" : {"Job" : "Clown", "Age" : 22},
"Sally" : {"Job" : "Programmer", "Age" : 32},
"Anne" : {"Job" : "Clown", "Age" : 29}
}
I would like to select everyone who is a Clown. My output should look like this:
{
"Joe" : {"Job" : "Clown", "Age" : 22},
"Anne" : {"Job" : "Clown", "Age" : 29}
}
I have tried the .. operator as in
cat people.json | jq '. | map(select(.Job == "Clown"))'
But it seems to match Joe and Anne at multiple levels and produces more output then I want. Any ideas? Thanks.
use with_entries to convert to/from an intermediate format that represents that data as an array of objects with key and value elements:
cat people.json | jq 'with_entries(select(.value.Job == "Clown"))'
as per the docs here: http://stedolan.github.io/jq/manual/
Here is a solution using reduce
. as $v
| reduce keys[] as $k (
{};
if $v[$k].Job == "Clown" then .[$k] = $v[$k] else . end
)