Data Navigation¶
JSON Path / Schema¶
Navigate JSON structures with JSONPath and validate them against JSON Schema.
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
nullablekeyword - Swagger 2.0, where the schema sits directly on the response, including its
x-nullableextension
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: