skip to Main Content

Say I’m getting an API response back that looks somewhat like this:

{
  "results": [
    {
      "name": "foobar",
      "description": "it's the foobar 1000 baby!"
    },
    {
      "name": "another_thing",
      "description": "acme 1000 whizbang machine"
    }
  ]
}

I pipe this object to the filter .results[] | select(.name == "foobar").description, but I’m not super confident I’ve written the select() filter completely correctly. How can I assert that the select() filter is functioning as intended, and returning exactly 1 result?

5

Answers


  1. Chosen as BEST ANSWER

    The select() filter emits a (possibly empty) stream of JSON objects, not a single array of JSON objects. Therefore while we can use length to do such an assertion, we have to be clever about it:

    [select(.name == "foobar").description]  | if length == 1 then .[] else "(length) results produced" | error end
    

    Of course you can do whatever you like instead of raising the error.

    The way this works is by wrapping the stream of objects returned by the select() filter in an array, so that the subsequent length filter doesn't operate on each object individually and instead operates on the list of objects. If the assertion passes, we unwrap the array again for the caller's convenience with .[].


  2. If you want an expression which is guaranteed not to produce MORE than one result, use first/1.

    If you want an expression which is guaranteed to produce exactly one result, assuming there are no errors preventing at least one result, you could perhaps use first in conjunction with //. In this case, of course, you’d have to specify what value you wanted in case the underlying query produces no results.

    Here’s an example:

    first(select(.name == "foobar" and has("description"))).description // "no description provided"
    

    A simpler alternative that might nevertheless be acceptable is:

    first(select(.name == "foobar").description // empty) // "no description provided"
    
    Login or Signup to reply.
  3. There are various functions to filter your input stream in a way that only certain results or a given number of them will be kept while others are being discarded.

    To (silently) discard a second and all subsequent results (i.e. getting only the first one or nothing), use first(f) as suggested by @peak. For the first n results there is the generalized limit($n; f) filter which provides you with all results until the nth one, i.e a stream of up to n items, thus limit(1; f) would act like first(f). Note that these filters (correctly) produce nothing if the stream was empty in the first place. You can test against this case using isempty(f) which produces a boolean as expected.

    To actively assert an exact (or a minimum or a maximum) number of results, i.e. take action if that fails, without collecting all items into a memory-consuming array just to query its length, you could count the size of the stream using reduce, and act upon evaluation of that result. Here’s a function that takes a number, an input stream which is reproduced (unaltered, i.e. as a stream) if the amount matches, and another filter that is produced instead if the counting fails. (Replace == used for exact matches with one of <, <=, >, >= to open up the upper or lower bounds.)

    def assert_n($n; f; g): if reduce f as $_ (0; .+1) == $n then f else g end;
    

    Applied to your test case:

    # produces exactly one result, namely "it's the foobar 1000 baby!"
    assert_n(1; .results[] | select(.name == "foobar").description; error("not one"))
    
    # errors out as select(true) happens to produce too many (>1) results
    assert_n(1; .results[] | select(true).description; error("not one"))
    
    # errors out as select(false) happens to produce too few (<1) results
    assert_n(1; .results[] | select(false).description; error("not one"))
    

    You can further generalize this approach by e.g. providing the counting result to a custom function.

    def assert_c(c; f; g): if reduce f as $_ (0; .+1) | c then f else g end;
    
    # produces an odd number of results, or errors out otherwise
    assert_c(.%2 == 1; .results[] | select(.name == "foobar"); error("was even"))
    
    Login or Signup to reply.
  4. If you have an array to start with, it’s probably more straightforward to use the map(select(f)) pattern:

    .results
    | map(select(.name == "foobar") | .description)
    | if length == 1 then first else error("expected exactly one result") end
    

    alternatively:

    .results
    | map(select(.name == "foobar") | .description)
    | select(length == 1)[0] // error("expected exactly one result")
    
    Login or Signup to reply.
  5. Here’s an "assertion"-style filter that you could use to avoid collecting all the results:

    def assert_one(s):
      reduce s as $x (null;
        if . == null then {emit: $x, n:1}
        else "assertion error: more than one"|error
        end )
      | select(.n == 1).emit 
      // ("assertion error: none"|error);
    

    Usage example:

    [{ name: "foobar", description: "yes"},
     { name: "foobar", description: "maybe"} ]
    | assert_one(.[] | select(.name == "foobar" and has("description")).description)
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search