Manny Adumbire
Manny Adumbire

Reputation: 396

How can jq be used to insert dynamic field names recursively for all objects in an array?

'm new to jq, and hoping to convert JSON below so that, for each object in the records array , the "Account" object is deleted and replaced with an "AccountID" field which has a the value of Account.Id. Assuming I don't know what the name of the field (eg. Account ) is prior to executing, so it Has to be dynamically included as an argument to --arg.

Contacts.json:

{
    "records": [
        {
            "attributes": {
                "type": "Contact",
                "referenceId": "ContactRef1"
            },
            "Account": {
                "attributes": {
                    "type": "Account",
                    "url": "/services/data/v51.0/sobjects/Account/asdf"
                },
                "Id": "asdf"
            }
    },
 {
            "attributes": {
                "type": "Contact",
                "referenceId": "ContactRef2"
            },
            "Account": {
                "attributes": {
                    "type": "Account",
                    "url": "/services/data/v51.0/sobjects/Account/qwer"
                },
                "Id": "qwer"
            }
    }
    ]

}

to

{
    "records": [
          {
            "attributes": {
                "type": "Contact",
                "referenceId": "ContactRef1"
            },
            "AccountID": "asdf"
            }
    },{
            "attributes": {
                "type": "Contact",
                "referenceId": "ContactRef2"
            },
            "AccountID": "qwer"
            }
    }
    ]

}

This example above is a little contrived because in actuality, I need to be able to dynamically name the ID field to be able to port the new JSON structure into destination system. For my use case, it's not always valid to tack "ID" onto the field name ( eg. Account .. ID ), so I passed the field names to --arg .

This is as close as I got.. but it's not quite there. and I suspect there is better way.

jq -c --arg field "Account" --arg field_name_id "AccountID" '.  |= .  + if .records?[]?[$field] != null then   { "\($field_name_id)" : .records[][$field].Id }  else empty end | if .records?[]?[$field] != null then del(.records[][$field]) else empty end'  Contacts.json

I've wrestled with this quite a while, but this is as far as I'm able to manage without running into tons of syntax errors. I really appreciate any help to add an AccountID field on each object in the records array.

Here's the actual bash script where jq is being run ( relevant parts are where FIELD(S) is being used )

#! /bin/bash
# This script takes a of soql file as first and only argument
# The main purpose is to tweak the json results from an sfdx:data:tree:export so the json is compatible with sfdx:data:tree:import
# This is needed because sfdx export & import are inadequate to use whne  relationships more than 2 levels deep in the export query.


# grab all unique object names within the soql file for any objects where the ID field is being SELECTed ( eg.  "Account Iteration__r Profile UserRole" )
FIELDS=`grep -oe '\([A-Za-z_]\+\)\.[iI][dD]'  $1 | cut -f 1 -d . - | sort -u`


#find all json files in file and rewrite the relationship FIELDS blocks into someting sfdx can import
for FIELD in $FIELDS;

do 

if [[ $FIELD =~ __r ]] 
then
 FIELD_NAME_ID=`sed 's/__r/__c/' <<< $FIELD`
else
FIELD_NAME_ID="${FIELD}ID" 
fi

JSON_FILES=`ls *.json`
#Loop all json files in direcotry
for DATA_FILE in $JSON_FILES
do

#replace any email addresses left in custom data( just in case ) 
#using gsed becuse Mac lacks -i flag for in-place substitution
gsed -i 's/[^@ "]*@[^@]*\.[^@ ,"]*/[email protected]/g' $DATA_FILE

# make temporary file to hold the rewritten json
TEMP_FILE="temp-${DATA_FILE}.bk"
echo $DATA_FILE $FIELD $FIELD_NAME_ID 

#For custom relationship jttrs. change __r to __c to get the name of Id field, otherwise just add "ID".
jq -c --arg field $FIELD --arg field_name_id $FIELD_NAME_ID '.  |= .  + if .records?[]?[$field] != null then   { "\($field_name_id)" : .records[][$field].Id }  else empty end | if .records?[]?[$field] != null then del(.records[][$field]) else empty end'  $DATA_FILE 1>  ./$TEMP_FILE  2> modify-json.errors

# if TEMP_FILE is not empty, then jq revised it, so replace contents the original JSON DATA_FILE 
if [[ -s ./$TEMP_FILE ]]
then
#JSON format spacing/line-breaks 
jq '.' $TEMP_FILE > $DATA_FILE
fi

rm $TEMP_FILE

done


done

Upvotes: 2

Views: 1125

Answers (2)

Charles Duffy
Charles Duffy

Reputation: 295687

Adapting peak's answer to use the dynamic field name:

jq -c --arg field "Account" \
      --arg field_name_id "AccountID" '
.records |= map(.[$field].Id as $x
                  | del(.[$field])
                  | . + {($field_name_id): $x})
'

Upvotes: 2

peak
peak

Reputation: 116919

The key to a simple solution is |=. Here's one using map:

.records |= map( .Account.Id as $x 
                 | del(.Account)
                 | . + {AccountID: $x} )

which can be simplified to:

.records |= map( . + {AccountID: .Account.Id}
                 | del(.Account) )

Either of these can easily be adapted to the case where the two field names are passed in as arguments, or if they must be inferred from the "owner" of "Id".

Upvotes: 4

Related Questions