Skip to content

Data Navigation

JSON Path / Schema

Navigate JSON structures with JSONPath and validate them against JSON Schema.

Optional dependency

pip install assertpy2[json]

at_json_path

Navigate to a JSONPath and keep asserting on the extracted value:

data = {"users": [{"name": "Alice"}, {"name": "Bob"}], "meta": {"total": 2}}

assert_that(data).at_json_path("$.meta.total").is_equal_to(2)
assert_that(data).at_json_path("$.users[0].name").is_equal_to("Alice")
assert_that(data).at_json_path("$.users[*].name").is_equal_to(["Alice", "Bob"])

Raises ValueError if the path does not exist.

has_json_path / does_not_have_json_path

Assert that a path is present or absent:

assert_that(data).has_json_path("$.meta.total")
assert_that(data).does_not_have_json_path("$.error")

matches_json_schema

Validate against a JSON Schema dict, or load it from a file:

schema = {
    "type": "object",
    "properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
    "required": ["name"],
}

assert_that({"name": "Alice", "age": 30}).matches_json_schema(schema)
assert_that(data).matches_json_schema_from_file("schemas/user.json")

JSON assertions chain and work with soft assertions:

with soft_assertions():
    assert_that(response).has_json_path("$.data")
    assert_that(response).at_json_path("$.data.id").satisfies(match.is_positive())

A path lands on whatever the document holds there, so what comes back offers the assertions every value has. To reach further, either use a matcher as above, or state the type you expect and continue in it.

The predicate below is a TypeIs, so the type checker takes its word. satisfies() runs it and reads the verdict. Neither can confirm that the claim matches the check inside.

from typing_extensions import TypeIs


def is_int(value: object) -> TypeIs[int]:
    return isinstance(value, int)


payload = {"data": {"id": 7}}
assert_that(payload).at_json_path("$.data.id").satisfies(is_int).is_between(1, 99)

conforms_to_openapi

Validate a response body against an OpenAPI operation's response schema - the one contract check none of the other assertion libraries here offer.

Three spec dialects are read:

  • OpenAPI 3.1, as written
  • OpenAPI 3.0, including its nullable keyword
  • Swagger 2.0, where the schema sits directly on the response, including its x-nullable extension

Inside any of them, $ref, oneOf / allOf / anyOf, enum and format validate with full JSON-Schema semantics.

Pass the parsed spec plus the operation's path and method. Loading the YAML or JSON is your job:

schema = {
    "type": "object",
    "required": ["id", "total"],
    "properties": {
        "id": {"type": "integer"},
        "total": {"type": "number"},
        "email": {"type": "string", "format": "email", "nullable": True},
    },
}
spec = {
    "openapi": "3.0.3",
    "paths": {
        "/orders/{id}": {
            "get": {
                "responses": {
                    "200": {"content": {"application/json": {"schema": schema}}}
                }
            }
        }
    },
}

body = {"id": 42, "total": 19.99, "email": None}
assert_that(body).conforms_to_openapi(spec, "/orders/{id}", "get")

path is the operation's key in the spec, not the request URL. A Swagger 2.0 basePath (or an OpenAPI 3 server prefix) is not part of it, so an endpoint served at /api/v1/version is looked up as /version.

status defaults to 200, then 201, then default. Pass status= to pick another.

When the body does not conform, the message names the operation and counts the failures. Every one is reported with its JSON path and the constraint it broke, not just the first:

diff (openapi):
  $:
    - {'id': 'x7', 'email': 'not-an-email'}
    + 'all required properties present'
  $.email:
    - 'not-an-email'
    + 'email format'
  $.id:
    - 'x7'
    + 'type integer'

Needs the JSON extra: pip install assertpy2[json].

Regex Group Extraction

extracting_group()

Search the value for a pattern and continue asserting on the captured group:

log = "2024-01-15 ERROR status=500 path=/api/users"

# by index
assert_that(log).extracting_group(r"status=(\d+)", 1).is_equal_to("500")
# by name
assert_that(log).extracting_group(r"(?P<level>\w+) status", "level").is_equal_to("ERROR")
# group 0 = whole match
assert_that("abc123").extracting_group(r"\d+").is_equal_to("123")
# chains
assert_that("count=42").extracting_group(r"count=(\d+)", 1).is_digit().is_length(2)

matches_with_groups()

Return all groups as a tuple, or a dict for named groups:

assert_that("2024-01-15 ERROR").matches_with_groups(
    r"(\d{4}-\d{2}-\d{2}) (\w+)"
).is_equal_to(("2024-01-15", "ERROR"))

assert_that("key=value").matches_with_groups(
    r"(?P<key>\w+)=(?P<val>\w+)"
).contains_entry({"key": "key"}).contains_entry({"val": "value"})