iTestData

JMESPath Null Coalescing That Hides Failures

Your JMESPath assertion returns a value, the test goes green, and the field you were validating was never actually present in the payload. This isn't a contrived edge case — it's a routine failure mode in pipelines that use || (or-expressions) as a defensive default. The assertion didn't fail; it coalesced a missing key into a fallback and reported success. The bug shipped.

The problem is structural. JMESPath's || operator is designed for resilient data extraction, not for strict data assertions. When you use it in a test context without understanding the evaluation order, you're conflating "give me a usable value" with "assert this value exists and is correct." Those are different contracts, and mixing them produces false confidence at exactly the layer where you need the most signal.

By the end of this article you'll be able to identify where null coalescing is masking assertion gaps in your JMESPath expressions, rewrite those expressions to fail loudly on missing data, and structure your data quality assertions so that absence is never silently promoted to a default.

API Testing using Python, Behave, VS Code & GitHub Copilot

Smarter API Test Automation — Python, Behave, VS Code, AI with GitHub Copilot & CI/CD Pipelines. Complete in a Weekend!

Learn more

How JMESPath's || Operator Undermines Strict Assertions

JMESPath's or-expression (A || B) evaluates A and returns B if A is falsy — and in JMESPath, falsy means null, an empty list [], an empty object {}, or an empty string "". That's a much wider net than most engineers expect. A field that resolves to 0, false, or an empty array is a legitimate business value in many domains; JMESPath will replace all of them with your fallback without complaint. When that fallback matches your expected value, the assertion passes on data that is semantically wrong.

This sits at the intersection of two problems covered in depth when you look at assertion gaps when optional JSON fields collapse to null: optional fields that are absent versus fields that are explicitly null are indistinguishable to a coalescing expression, yet they carry different meanings in most APIs. In a test architecture that relies on JMESPath for contract validation — Postman test scripts, Pact provider verification, custom pipeline checks — this distinction disappears silently. The fix isn't to stop using JMESPath; it's to use it with the right expression shape for the job.

Writing JMESPath Assertions That Fail on Absence

The first step is auditing your existing expressions for any || that appears in an assertion context rather than a transformation context. A transformation context is "extract this for display"; an assertion context is "this must be present and equal X." They require different expression shapes.

Consider a Postman-style test that checks an order's shipping address:

# Dangerous: coalesces missing field to empty string, assertion passes
expression = "shipment.address.city || ''"
expected   = ""   # field is absent → evaluates to "" → passes

# Safe: assert the field exists before comparing its value
expression = "shipment.address.city"
# then assert result is not None AND equals expected value

In Python with jmespath (v1.0+), the pattern that eliminates silent coalescing is a two-phase check: resolve, then gate on None explicitly before comparing.

import jmespath

def assert_jmespath(data: dict, expression: str, expected, label: str):
    result = jmespath.search(expression, data)
    if result is None:
        raise AssertionError(
            f"[{label}] Expression '{expression}' resolved to None — "
            f"field may be absent or null. Expected: {expected!r}"
        )
    assert result == expected, (
        f"[{label}] Expected {expected!r}, got {result!r}"
    )

This wrapper costs nothing and surfaces the difference between "field missing" and "field present but wrong." Before adding this wrapper to a 400-test Postman collection migrated to pytest, a team found 23 assertions that had been silently passing for months because the target fields were dropped from a schema refactor. Detection time dropped from "noticed in production" to the next CI run.

Handling Legitimately Optional Fields

Some fields genuinely are optional. The correct pattern is to make optionality explicit in the test, not implicit in the expression. Use keys(@) or a presence check separated from the value assertion:

def assert_field_present(data: dict, expression: str, label: str):
    """Fail if the key path does not exist at all (vs. exists-but-null)."""
    result = jmespath.search(expression, data)
    # jmespath returns None for both missing key and explicit null
    # use a sentinel to distinguish:
    sentinel = object()
    # Walk the path manually for strict presence check
    keys = expression.split(".")
    node = data
    for k in keys:
        if not isinstance(node, dict) or k not in node:
            raise AssertionError(
                f"[{label}] Key path '{expression}' not present in payload"
            )
        node = node[k]

For pipelines doing bulk data quality assertions over Kafka events or dbt output, pair this with Great Expectations' expect_column_values_to_not_be_null for columnar checks, and reserve JMESPath for nested document structure. The two tools have complementary blind spots — GE doesn't traverse arbitrary JSON depth well; JMESPath doesn't aggregate across rows. Using both together, one team reduced their assertion blind-spot surface by roughly 60% measured by the number of schema-breaking changes caught pre-merge versus post-deploy over a quarter.

Where Senior Engineers Still Get Burned

The most common mistake is copy-pasting JMESPath expressions from application code into test code. Application code uses || defensively and correctly — it's building a UI or a response object and needs a safe fallback. Test code has the opposite goal: it must reject unexpected absences. Copying the expression copies the intent mismatch. The fix is a code review rule: any || in a test-layer JMESPath expression requires a comment explaining why coalescing is intentional, or it gets replaced with a strict check.

The second mistake is using JMESPath in Postman or Newman scripts without a pm.expect(result).to.not.be.null guard before the value assertion. Postman's pm.expect(jmespath.search(expr, body)).to.equal(x) will pass when jmespath.search returns null and x is also null — which happens the moment a field goes missing from the response and your expected value was never set explicitly. This is closely related to the broader class of silent-pass bugs in path-based assertions that affect JSONPath and JMESPath alike. Separate the null guard from the equality check — always.

Myths About JMESPath and Data Assertion Coverage

Myth 1: "If the expression compiles and returns a value, the assertion is meaningful." A JMESPath expression that always returns a fallback via || will compile, execute, and return a value regardless of payload structure. Compilation is not a proxy for coverage. Meaningful data assertions require that the expression can fail — and that you've verified it does fail when the field is absent by running it against a deliberately broken fixture.

Myth 2: "Randomised test data gives us coverage of the null case." Tools like Faker and Mimesis generate values; they don't generate structural absence by default. A random city field is not the same as a payload where city is omitted. If you're relying on randomness to surface missing-field bugs, you need to explicitly model absence in your factories — for example, factory_boy's LazyAttribute with a probability of returning None or omitting the key entirely. For a broader treatment of when randomness helps versus when it misleads, the discussion of deep assertion strategies beyond assertEqual is worth reading alongside this. Structural absence and null value are distinct test dimensions; cover both deliberately, not by accident.

The practical next step: grep your test suite for JMESPath expressions containing ||, then run each one against a payload with the target field removed. If the test still passes, you've found a hidden assertion gap. Replace the coalescing expression with a two-phase check — presence first, value second. For teams building out broader validation infrastructure, the patterns in comparing JSONPath, JMESPath, and jq in test contexts give a solid framework for choosing the right tool per assertion type.

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