mappingjq

Mapping from file in jq


I'm trying to use jq to look up data from a json file with account mappings. So given an account number as input, I want to look up it's name from a file, and it needs to handle the account not being found.

echo '["1100", "1350"]' | jq --slurpfile accountsdata accounts.json '
    def get_account_data(account):
        [$accountsdata[]
        | .[]
        | select((.accountnumber | tostring) == (account | tostring))] as $output
        | if $output | length == 0 then {"name": ("no mapping for " + account + ".")} else $output[0] end;

    def get_name(account):
        get_account_data(account).name;

. | map(get_name(.))
'

The account mapping file is as follows:

[
  {
    "accountnumber": 1100,
    "name": "Account A"
  },
  {
    "accountnumber": 1350,
    "name": "Account B"
  },
  {
    "accountnumber": 1356,
    "name": "Account C"
  }
]

Expected output:

[
  "Account A",
  "Account B"
]

Actual output:

[
  "no mapping for 1100.",
  "no mapping for 1350."
]

However, if I change the call from . | map(get_name(.)) to . | map(get_name("1100")) (i.e. hardcode the accountnumber), I get the expected

[
  "Account A",
  "Account A"
]

What am I missing?


Solution

  • What am I missing?

    The function get_account_data(account) defines account as a function, so it won't be evaluated immediately. Thus, when calling get_account_data with . as the argument, any reference of account inside of it will be substituted by the function ., and then evaluated in that target context. So, (.accountnumber | tostring) == (account | tostring) would become (.accountnumber | tostring) == (. | tostring), with . being evaluated to the current context, which by then is different to the items from the input (which has been the context at the time of calling the function). To mitigate this, call the function by value by defining its argument as a variable, i.e. get_account_data($account). This way, it is the value being passed over, so (.accountnumber | tostring) == ($account | tostring) becomes (.accountnumber | tostring) == ("1100" | tostring) in the first iteration, etc.

    Here's a simplified version of your approach:

    echo '["1100", "1234", "1350"]' | jq --slurpfile accountsdata accounts.json '
    
      def get_account_data($account):
        [ $accountsdata[][]
          | select(.accountnumber | tostring == $account)
        ] as $output
        | if $output | length == 0 then {name: "no mapping for \($account)."}
          else $output[0] end;
    
      def get_name:
        get_account_data(.).name;
    
      map(get_name)
    '
    
    [
      "Account A",
      "no mapping for 1234.",
      "Account B"
    ]
    

    However, I'd rather create a lookup object which you can query repeatedly without iterating over the accounts data again and again for each input number. For creating such object, you could employ the INDEX function, and combine it with JOIN to produce the matches, while defaulting with // to another expression if there's no match, as accessing a missing object key (i.e. performing an unsuccessful lookup) always evaluates to null:

    echo '["1100", "1234", "1350"]' | jq --slurpfile accountsdata accounts.json '[
      JOIN(INDEX($accountsdata[][]; .accountnumber);
        .[]; .; last.name // "no mapping for \(first)."
      )
    ]'
    
    [
      "Account A",
      "no mapping for 1234.",
      "Account B"
    ]