Home > Net >  jq - check for subkey without filtering
jq - check for subkey without filtering

Time:05-31

I would like to check for the presence of a subkey (or for its type) in a JSON file using jq without filtering the data. I need this to obtain a list of all entities from the following JSON file, with the first pair of coordinates if more than one pair is available. The problem is the partly nested structure: The "location" object is only available for some entries, and the "coordinates" element is an array only if multiple locations are available.

{"records":[
{
    "id": 1,
    "location": {
        "coordinates": {
            "lat": 42,
            "lon": -71
        }
    }
},
{
    "id": 2,
    "location": {
        "coordinates": [
            {
                "lat": 40,
                "lon": -73
            },
            {
                "lat": 39,
                "lon": -75
            }
        ]
    }
},
{
    "id": 3,
    "location": null
}]}

So I tried the "has" function, which does not seem to work for subkeys. I imagined something like this:

cat file.json | jq '.records[] | if has("location.coordinates") then [do something] else [do something else] end'

Is there any way to check for subkeys? As I need to maintain all entries in the dataset, filtering via "select" etc. does not seem to be an option.

To clarify my question: I hoped to get a JSON output similar to this (but I would be happy to handle other formats):

{"records":[
{"id": 1, "lat", xx, "lon": xx}
{"id": 2, "lat", yy, "lon": yy}
{"id": 3, "lat", null, "lon": null}
]}

CodePudding user response:

Instead off has, you can use a select with an |= opertator:

.records |= map(.id as $id | (.location.coordinates | (if type == "array" then .[0] else . end) as $q | ({ $id, lat: null, lon: null }   $q) ))

This generates:

{
  "records": [
    {
      "id": 1,
      "lat": 42,
      "lon": -71
    },
    {
      "id": 2,
      "lat": 40,
      "lon": -73
    },
    {
      "id": 3,
      "lat": null,
      "lon": null
    }
  ]
}

As you can try in this online demo.


So, the above explained:

  1. Loop over records and update them
    .records |= map()
  2. Save the id
    .id as $id
  3. Continue on the coordinates
    .location.coordinates |
  4. Check for an array, if so, get the fist object, otherwise, just keep it, save as $q
    (if type == "array" then .[0] else . end) as $q
  5. Create the final object, start with an object with just the id and lat and lon set to null, then we combine $q to get the actual values from the coordinates
    ({ $id, lat: null, lon: null } $q)

This could most probably be simplified, so waiting for another answer that has the same idea, but more optimised.

CodePudding user response:

You can also use alternative operator // :

jq '.records[] |= {id} ((.location.coordinates? |
                         if type == "array" then .[0] else . end
                        ) // {}
                       )' input.json

CodePudding user response:

You could use the error suppression operator ? in combination with the alterative operator // to fall back to another case if the previous one fails:

.records[] |= (.location.coordinates | .[0]? // .) as {$lat,$lon} | {id,$lat,$lon}
{
  "records": [
    {
      "id": 1,
      "lat": 42,
      "lon": -71
    },
    {
      "id": 2,
      "lat": 40,
      "lon": -73
    },
    {
      "id": 3,
      "lat": null,
      "lon": null
    }
  ]
}

Demo

  • Related