Group related items then merge them and accumulate some properties into arrays - json

Have a JSON as below,
[
{
"first": "foo",
"last": "bar",
"roll": "32",
"subject": "maths"
},
{
"first": "joe",
"last": "mighty",
"roll": "31",
"subject": "english"
},
{
"first": "foo",
"last": "bar",
"roll": "32",
"subject": "english"
},
{
"first": "joe",
"last": "mighty",
"roll": "31",
"subject": "maths"
},
{
"first": "foo",
"last": "bar",
"roll": "32",
"subject": "science"
}
]
unique_by(.first,.last, .roll) will remove other subject entries. I want convert 'subject' to an array with all the values in the source JSON array. How to map using jq for creating nested array of subject as below,
[
{
"first": "foo",
"last": "bar",
"roll": "32",
"subject": ["maths", "english", "science"]
},
{
"first": "joe",
"last": "mighty",
"roll": "31",
"subject": ["english", "maths"]
}
]

JQ doesn't have a nice merge built-in for such tasks yet, but you can achieve this one using group_by.
group_by(.first, .last, .roll) | map(
(.[0] | {first, last, roll}) + {subject: map(.subject)}
)
Online demo

A variation based on Oguz's excellent answer:
group_by(.first, .last, .roll)
| map( map(.subject) as $subject
| .[0]
| {first, last, roll, $subject})
Collect related items (group by first, last and roll) and reduce each collection into a single item:
First we put all subjects into $subject
Take the first item as a "template" but rewrite the .subject key with $subject.

Related

JQ - unique count of each value in an array

I'm needing to solve this with JQ. I have a large lists of arrays in my json file and am needing to do some sort | uniq -c types of stuff on them. Specifically I have a relatively nasty looking fruit array that needs to break down what is inside. I'm aware of unique and things like that, and imagine there is likely a simple way to do this, but I've been trying run down assigning things as variables and appending and whatnot, but I can't get the most basic part of counting the unique values per that fruit array, and especially not without breaking the rest of the content (hence the variable ideas). Please tell me I'm overthinking this.
I'd like to turn this;
[
{
"uid": "123abc",
"tID": [
"T19"
],
"fruit": [
"Kiwi",
"Apple",
"",
"",
"",
"Kiwi",
"",
"Kiwi",
"",
"",
"Mango",
"Kiwi"
]
},
{
"uid": "456xyz",
"tID": [
"T15"
],
"fruit": [
"",
"Orange"
]
}
]
Into this;
[
{
"uid": "123abc",
"tID": [
"T19"
],
"metadata": [
{
"name": "fruit",
"value": "Kiwi - 3"
},
{
"name": "fruit",
"value": "Mango - 1"
},
{
"name": "fruit",
"value": "Apple - 1"
}
]
},
{
"uid": "456xyz",
"tID": [
"T15"
],
"metadata": [
{
"name": "fruit",
"value": "Orange - 1"
}
]
}
]
Using group_by and length would be one way:
jq '
map(with_entries(select(.key == "fruit") |= (
.value |= (group_by(.) | map(
{name: "fruit", value: "\(.[0] | select(. != "")) - \(length)"}
))
| .key = "metadata"
)))
'
[
{
"uid": "123abc",
"tID": [
"T19"
],
"metadata": [
{
"name": "fruit",
"value": "Apple - 1"
},
{
"name": "fruit",
"value": "Kiwi - 4"
},
{
"name": "fruit",
"value": "Mango - 1"
}
]
},
{
"uid": "456xyz",
"tID": [
"T15"
],
"metadata": [
{
"name": "fruit",
"value": "Orange - 1"
}
]
}
]
Demo

Searching through an array

Trying to learn how to think in jq script.
Given this data:
{
"characters": [
{ "First": "Fred", "Last": "Weasley" },
{ "First": "George", "Last": "Weasley" },
{ "First": "Hermione", "Last": "Granger" },
{ "First": "Ron", "Last": "Weasley" },
{ "First": "Hagrid" },
{ "First": "Draco", "Last": "Malfoy" },
{ "First": "Molly", "Last": "Weasley" },
{ "First": "Voldemort" },
{ "First": "Lucius", "Last": "Malfoy" }
]
}
Find all characters with the same last name as "Ron". And no, you don't already know his last name.
Find the last name you are looking for and save it in a variable, then update the characters array accordingly:
jq '
(.characters[] | select(.First == "Ron").Last) as $last
| .characters |= map(select(.Last == $last))
'
{
"characters": [
{
"First": "Fred",
"Last": "Weasley"
},
{
"First": "George",
"Last": "Weasley"
},
{
"First": "Ron",
"Last": "Weasley"
},
{
"First": "Molly",
"Last": "Weasley"
}
]
}
Demo
If you want to make the call with a dynamic first name, provide it using a parameter variable:
jq --arg first "Ron" '
(.characters[] | select(.First == $first).Last) as $last
| .characters |= map(select(.Last == $last))
'
You have an array of objects called "characters" each containing a "first" and "last" variable.
Now to get the first one. Go characters[0].first which would return. "Fred"
Or characters.[3].last would return "Weasley"
Note the first entry in array is fetched by index 0.
Make sense?

Add key value to parent subelement if child has specific key:value

I'm trying to understand what's the best way to add a json element to child's parent
if that child contains a specific key:value and finally print the entire json using jq
I try to explain better with an example.
The input json is:
{
"family": {
"surname": "Smith"
},
"components": [
{
"name": "John",
"details": {
"hair": "brown",
"eyes": "brown",
"age": "56"
},
"role": "father"
},
{
"name": "Mary",
"details": {
"hair": "blonde",
"eyes": "green",
"age": "45"
},
"role": "mother"
},
{
"name": "George",
"details": {
"hair": "blonde",
"eyes": "brown",
"age": "25"
},
"role": "child"
}
]
}
I want to add:
"description": "5 years less than 30"
at the same level of "details" if "age" is equal to "25" and then print the result:
{
"family": {
"surname": "Smith"
},
"components": [
{
"name": "John",
"details": {
"hair": "brown",
"eyes": "brown",
"age": "56"
},
"role": "father"
},
{
"name": "Mary",
"details": {
"hair": "blonde",
"eyes": "green",
"age": "45"
},
"role": "mother"
},
{
"name": "George",
"details": {
"hair": "blonde",
"eyes": "brown",
"age": "25"
},
"role": "child",
"description": "5 years less than 30"
}
]
}
The only solution I've found was to apply the update but printing only the "components" content;
then I've removed from the JSON and finally inserted the modified "components" content previously saved, in this way:
cat sample.json | jq -c ' .components[] | select(.details.age=="25") |= . + {description: "5 years less than 30" } ' > /tmp/saved-components.tmp
cat sample.json | jq --slurpfile savedcomponents /tmp/saved-components.tmp 'del(.components) | . + { components: [ $savedcomponents ] }'
I don't think it's the best way to solve these kind of problems, so I'd like to know what is
the right "jq approach".
I forgot to say: I prefer to use jq only, not other tools
Than you
Marco
You can select the object matching the condition and append to that object. Something like below. The key is to use += the modification assignment to not lose the other objects
(.components[] | select(.details.age == "25")) += { "description": "5 years less than 30" }
jqplay - Demo
Here's a straightforward ("no magic") and efficient solution:
.components |=
map(if .details.age=="25" then .description = "5 years less than 30" else . end)

Using jq to convert object to key with values

I have been playing around with jq to format a json file but I am having some issues trying to solve a particular transformation. Given a test.json file in this format:
[
{
"name": "A", // This would be the first key
"number": 1,
"type": "apple",
"city": "NYC" // This would be the second key
},
{
"name": "A",
"number": "5",
"type": "apple",
"city": "LA"
},
{
"name": "A",
"number": 2,
"type": "apple",
"city": "NYC"
},
{
"name": "B",
"number": 3,
"type": "apple",
"city": "NYC"
}
]
I was wondering, how can I format it this way using jq?
[
{
"key": "A",
"values": [
{
"key": "NYC",
"values": [
{
"number": 1,
"type": "a"
},
{
"number": 2,
"type": "b"
}
]
},
{
"key": "LA",
"values": [
{
"number": 5,
"type": "b"
}
]
}
]
},
{
"key": "B",
"values": [
{
"key": "NYC",
"values": [
{
"number": 3,
"type": "apple"
}
]
}
]
}
]
I have followed this thread Using jq, convert array of name/value pairs to object with named keys and tried to group the json using this expression
jq '. | group_by(.name) | group_by(.city) ' ./test.json
but I have not been able to add the keys in the output.
You'll want to group the items at the different levels and building out your result objects as you want.
group_by(.name) | map({
key: .[0].name,
values: (group_by(.city) | map({
key: .[0].city,
values: map({number,type})
}))
})
Just keep in mind that group_by/1 yields groups in a sorted order. You'll probably want an implementation that preserves that order.
def group_by_unsorted(key_selector):
reduce .[] as $i ({};
.["\($i|key_selector)"] += [$i]
)|[.[]];

bash JQ. How can modify a key value pair from json file containing list of objects?

I am using jq to work on a large json file. It looks something like this:
FILE1.json
{
"person": [
{
"name": "sam",
"age": "40",
"weight": "180",
"height": "6"
},
{
"name": "peter",
"age": "41",
"weight": "180",
"height": "6.1"
},
{
"name": "mike",
"age": "40",
"weight": "200",
"height": "5.9"
},
{
"name": "ethan",
"age": "41",
"weight": "190",
"height": "6"
}
]
}
I want to use jq tool to change the value of weight from 200 to 195 where name is "mike".
How can i do this?
The idea is to update the person array where the object that has the name "mike" will be modified to have the weight "195". Otherwise it's just skipped.
.person |= map(
if .name == "mike"
then .weight = "195"
else .
end)
Or more concisely, search for the persons to update and update them:
(.person[] | select(.name == "mike")).weight = "195"