iTestData

JSONPath Wildcards That Swallow Missing Fields

Your JSONPath assertion returns a non-empty result set, your test goes green, and the field you were validating was never in the payload. This is not a hypothetical — it happens every time you write $.items[*].price against a response where price is conditionally omitted by the API. The wildcard matches every element; the missing key is simply skipped; the result list is shorter than expected and nobody checks the length. The test passes. The bug ships.

The root problem is that JSONPath's wildcard and recursive-descent operators are projection operators, not existence operators. They return whatever is there and discard the rest — silently. Most assertion libraries compound this by checking only that the result is truthy, not that it has the cardinality you intended. This is a different failure mode from array-order shifts breaking assertions, but it shares the same root cause: treating a projection as a structural guarantee.

By the end of this article you will be able to identify every wildcard pattern that can swallow a missing field, write cardinality-aware assertions that catch the gap, and structure your test data so the omission surfaces at generation time rather than assertion time.

Finding Undervalued Players: The Method

Explore the data, models, mistakes, and methods behind identifying overlooked players.

Learn more

Why Wildcards Are Projection Operators, Not Existence Checks

JSONPath (both the original Goessner spec and the RFC 9535 formalization) defines [*], ..*, and the slice operator as node-list producers. Given a path like $.orders[*].shipping.trackingId, the engine walks every element of orders, descends into shipping, and yields trackingId where it exists. An element that lacks shipping entirely contributes zero nodes to the output — no error, no null placeholder, no sentinel. The result list is simply shorter.

This matters architecturally because most assertion helpers — Postman's pm.expect(jsonData).to.have.nested.property, Python's jsonpath-ng, and even Schemathesis's response checks — evaluate truthiness on the result list, not its length relative to the input collection. If your orders array has 5 elements and only 3 carry trackingId, a wildcard assertion returns 3 matches and passes. Pairing this with optional fields that collapse to null makes the situation worse: the field may be present-but-null in some elements and absent entirely in others, giving you two distinct failure modes under one green test.

Cardinality-Aware Assertions and Generation-Time Guards

The fix has two layers: assert on count, not just on presence; and generate test data that forces the absent-field path explicitly. Start with count enforcement in jsonpath-ng:

from jsonpath_ng.ext import parse
import json

payload = json.loads(response.text)
expr = parse("$.orders[*].shipping.trackingId")
matches = expr.find(payload)

expected_count = len(payload["orders"])   # every order must have one
assert len(matches) == expected_count, (
    f"trackingId present in {len(matches)}/{expected_count} orders — "
    f"missing in indices: {[i for i, o in enumerate(payload['orders']) "
    f"if 'trackingId' not in (o.get('shipping') or {})]}"
)

The error message does the forensic work for you — it names the exact indices rather than just failing with a count mismatch. The jsonpath-ng 1.6+ ext module is required for filter expressions; without it, parse() silently falls back to a subset of the spec.

For JMESPath (AWS SDKs, Boto3 waiters, many API gateway tests), the equivalent pattern uses length() and not_null() together, because JMESPath projects nulls rather than omitting nodes — a different but equally deceptive behavior covered in the broader comparison of JSONPath, JMESPath, and jq:

import jmespath

expr_all   = jmespath.compile("orders[*]")
expr_track = jmespath.compile("orders[?shipping.trackingId != null].shipping.trackingId")

all_orders  = expr_all.search(payload) or []
with_track  = expr_track.search(payload) or []

assert len(with_track) == len(all_orders), (
    f"{len(all_orders) - len(with_track)} orders missing trackingId"
)

Generation-time enforcement is the second layer and often the more valuable one. If your factories produce test payloads, make the absent-field scenario an explicit variant rather than an accidental one. With factory_boy:

import factory
from factory import SubFactory, LazyAttribute
import random

class ShippingFactory(factory.DictFactory):
    trackingId = factory.Sequence(lambda n: f"TRK-{n:06d}")
    carrier    = "FedEx"

class OrderFactory(factory.DictFactory):
    id       = factory.Sequence(lambda n: n)
    amount   = factory.Faker("pydecimal", left_digits=4, right_digits=2, positive=True)
    shipping = SubFactory(ShippingFactory)

class OrderWithoutTrackingFactory(OrderFactory):
    """Explicit variant — shipping present, trackingId absent."""
    shipping = LazyAttribute(lambda _: {"carrier": "FedEx"})  # no trackingId key

class OrderWithoutShippingFactory(OrderFactory):
    """Explicit variant — shipping block absent entirely."""
    shipping = None

Running your assertion suite against all three factory variants — nominal, missing key, missing block — in a single parametrized Pytest case catches the wildcard-swallow bug before it reaches CI. Generation went from ad-hoc dict literals scattered across 40 test files to three factory classes; the parametrize decorator does the rest. Teams that adopted this pattern reported eliminating an entire class of "green-but-wrong" assertion failures in their contract test suites within one sprint.

Pitfalls Senior Engineers Still Hit With Wildcard Paths

Anchoring on a happy-path payload size. The most common mistake is writing the expected count as a literal — assert len(matches) == 5 — because the fixture always has five orders. When the API starts returning paginated responses with variable page sizes, the literal breaks on every non-standard page, so engineers remove the count check entirely rather than fix it. The right move is to derive the expected count from the same payload you are asserting against, as shown above. The count check becomes self-consistent and survives pagination changes.

Trusting recursive descent (..*) as a field existence probe. $..* is frequently used to "find a field anywhere in the document" — and it works until the field is absent, at which point it returns an empty list that most assertion helpers treat as falsy-but-not-an-error. This happens because ..* feels like a grep, but it is still a projection. Pair it with an explicit length check or, better, use a JSON Schema required constraint validated with jsonschema 4.x for structural guarantees; reserve JSONPath for value-level assertions. The hidden cost of bad test data compounds here: a structural gap missed in unit tests surfaces as a production incident traced back to an assertion that was never actually checking what the author thought.

Myths About Wildcard Coverage That Lead Teams Astray

Myth: a passing wildcard assertion means the field is present in every element. It means the field was present in at least one element — or in exactly zero elements if your assertion library only checks for a non-empty result. RFC 9535 Section 2.3.5 is explicit: node-list operators yield zero nodes for absent keys and this is not an error condition. If your contract requires the field in every element, you need a count equality check, a JSON Schema required on the array item schema, or a Schemathesis stateful test that validates each item independently.

Myth: adding more wildcard paths increases assertion coverage. Adding $.items[*].id, $.items[*].sku, and $.items[*].price as three separate assertions does not give you three times the coverage if all three can silently short-circuit on the same missing element. You have three green assertions and one structurally incomplete item. The correct model is to validate item-level structure once — with a JSON Schema $defs reference and jsonschema.validate() — and use JSONPath only for value-level spot checks where schema validation is too coarse. Generating explicit missing-field variants via factory patterns ensures those schema checks are exercised against the failure case, not just the happy path.

Wildcard paths are useful for value extraction; they are unreliable as existence probes. The fix is mechanical: derive expected counts from the input collection, assert equality, and generate explicit absent-field variants in your factories. Add a JSON Schema layer for structural guarantees and keep JSONPath for what it does well — targeted value retrieval. If you want to go deeper, RFC 9535 is the authoritative spec and is worth an hour of your time; the normative grammar alone will change how you read wildcard expressions.

Note: This article is for informational purposes only and is not a substitute for professional advice. If you need guidance on specific situations described in this article, consider consulting a qualified professional.

Understanding how systems actually work is the first step toward navigating them effectively.

Browse all articles