Bash – Create JSON Object from Associative Array Using jo

associative arraybashjqjson

I know about How to create JSON from associative array but that's not my problem.

I have this associative array:

declare -A aliases
aliases[Index]=components/Index/Exports
aliases[Shared]=components/Shared/Exports
aliases[Icons]=components/Icons/Exports

Now I need to convert this associative array into this JSON:

{
    "compilerOptions": {
        "baseUrl": ".",
        "paths": {
            "Index": ["components/Index/Exports"],
            "Shared": ["components/Shared/Exports"],
            "Icons": ["components/Icons/Exports"],
        }
    }
}

I want to use jo and jq. But I can't come up with the nesting.

I tried this code:

jo -p compilerOptions[baseUrl]=. compilerOptions[paths]="$(jo -p a ${Aliases[@]})"

But it does not even run.

Best Answer

One possible solution to this:

declare -A aliases
aliases[Index]=components/Index/Exports
aliases[Shared]=components/Shared/Exports
aliases[Icons]=components/Icons/Exports

jq -n --argjson n "${#aliases[@]}" '
        { compileroption: {
                baseurl: ".",
                paths:
                (
                        reduce range($n) as $i ({};
                                .[$ARGS.positional[$i]] = [$ARGS.positional[$i+$n]]
                        )
                )
        } }' --args "${!aliases[@]}" "${aliases[@]}"

Does not use jo and instead pass the keys and values of the associative array aliases into jq as positional parameters with --args at the end of the command (--args must always be the last option, if it's used at all). The jq utility receives the keys and values as a single array, $ARGS.positional. This means the first half of the array contains the keys, and the second half of the array contains the corresponding values.

The body of the jq expression creates the output object and uses a reduce operation over a range of $n integers from zero up, where $n is the number of elements in the aliases array. The reduce operation builds the paths object by adding the positional arguments, one by one, using $i:th argument as the key and the $i+$n:th argument as the element in the corresponding array value.


A slightly different approach using jo to create leaf objects of each key-value pair of the associative array:

declare -A aliases
aliases[Index]=components/Index/Exports
aliases[Shared]=components/Shared/Exports
aliases[Icons]=components/Icons/Exports

for key in "${!aliases[@]}"; do
        jo "$key[]=${aliases[$key]}"
done

This would output the three objects

{"Icons":["components/Icons/Exports"]}
{"Index":["components/Index/Exports"]}
{"Shared":["components/Shared/Exports"]}

Since we're using jo like this, we impose some obvious restrictions on the keys of the array (may not contain =, [] etc.)

We could use jq in place of jo like so:

for key in "${!aliases[@]}"; do
        jq -n --arg key "$key" --arg value "${aliases[$key]}" '.[$key] = [$value]'
done

We may then read these and add them in the correct place in the object we're creating in jq:

declare -A aliases
aliases[Index]=components/Index/Exports
aliases[Shared]=components/Shared/Exports
aliases[Icons]=components/Icons/Exports

for key in "${!aliases[@]}"; do
        jo "$key[]=${aliases[$key]}"
done |
jq -n '{ compileroptions: {
        baseURL: ".",
        paths: (reduce inputs as $item ({}; . += $item)) } }'

The main difference here is that we don't pass stuff into jq as command line options, but rather as a stream of JSON objects.