Skip to content

JSON assertions

JSONPath navigation and JSON Schema validation (requires the json extra).

JSON path navigation and schema validation mixin.

at_json_path

at_json_path(path: str) -> Self

Navigate to a JSON path and return a new builder with the matched value.

Uses JSONPath syntax (e.g. $.users[0].name). If multiple matches are found, the value is a list of all matches. If exactly one match is found, the value is unwrapped from the list.

Parameters:

Name Type Description Default
path str

JSONPath expression.

required

Examples:

Usage:

data = {"users": [{"name": "Alice"}, {"name": "Bob"}]}
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"])

Returns:

Name Type Description
AssertionBuilder Self

a new instance with the extracted value

Raises:

Type Description
ValueError

if no match is found at the given path

Source code in assertpy2/json_mixin.py
def at_json_path(self, path: str) -> Self:
    """Navigate to a JSON path and return a new builder with the matched value.

    Uses JSONPath syntax (e.g. ``$.users[0].name``). If multiple matches are found,
    the value is a list of all matches. If exactly one match is found, the value is
    unwrapped from the list.

    Args:
        path: JSONPath expression.

    Examples:
        Usage:

            data = {"users": [{"name": "Alice"}, {"name": "Bob"}]}
            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"])

    Returns:
        AssertionBuilder: a new instance with the extracted value

    Raises:
        ValueError: if no match is found at the given path
    """
    expr = _parsed_json_path(path)
    # a JSON path walks a decoded document. Handed a scalar, jsonpath answers "'int' object is not
    # subscriptable", which is about its own indexing rather than about the value under assertion
    require_type(self.val, (dict, list), "a decoded JSON document (a dict or a list)")
    matches = expr.find(self.val)
    if not matches:
        raise ValueError(f"Expected JSON path <{path}> to exist, but it did not.")
    if len(matches) == 1:
        return self.builder(matches[0].value, self.description, self.kind)
    return self.builder([match.value for match in matches], self.description, self.kind)

has_json_path

has_json_path(path: str) -> Self

Assert that the given JSON path exists in val.

Parameters:

Name Type Description Default
path str

JSONPath expression.

required

Examples:

Usage:

data = {"meta": {"total": 5}}
assert_that(data).has_json_path("$.meta.total")

Returns:

Name Type Description
AssertionBuilder Self

returns this instance to chain to the next assertion

Raises:

Type Description
AssertionError

if the path does not exist

Source code in assertpy2/json_mixin.py
def has_json_path(self, path: str) -> Self:
    """Assert that the given JSON path exists in val.

    Args:
        path: JSONPath expression.

    Examples:
        Usage:

            data = {"meta": {"total": 5}}
            assert_that(data).has_json_path("$.meta.total")

    Returns:
        AssertionBuilder: returns this instance to chain to the next assertion

    Raises:
        AssertionError: if the path does not exist
    """
    expr = _parsed_json_path(path)
    # a JSON path walks a decoded document. Handed a scalar, jsonpath answers "'int' object is not
    # subscriptable", which is about its own indexing rather than about the value under assertion
    require_type(self.val, (dict, list), "a decoded JSON document (a dict or a list)")
    matches = expr.find(self.val)
    if not matches:
        return self.error(f"Expected JSON path <{path}> to exist, but it did not.")
    return self

does_not_have_json_path

does_not_have_json_path(path: str) -> Self

Assert that the given JSON path does not exist in val.

Parameters:

Name Type Description Default
path str

JSONPath expression.

required

Examples:

Usage:

data = {"status": "ok"}
assert_that(data).does_not_have_json_path("$.error")

Returns:

Name Type Description
AssertionBuilder Self

returns this instance to chain to the next assertion

Raises:

Type Description
AssertionError

if the path exists

Source code in assertpy2/json_mixin.py
def does_not_have_json_path(self, path: str) -> Self:
    """Assert that the given JSON path does not exist in val.

    Args:
        path: JSONPath expression.

    Examples:
        Usage:

            data = {"status": "ok"}
            assert_that(data).does_not_have_json_path("$.error")

    Returns:
        AssertionBuilder: returns this instance to chain to the next assertion

    Raises:
        AssertionError: if the path exists
    """
    expr = _parsed_json_path(path)
    # a JSON path walks a decoded document. Handed a scalar, jsonpath answers "'int' object is not
    # subscriptable", which is about its own indexing rather than about the value under assertion
    require_type(self.val, (dict, list), "a decoded JSON document (a dict or a list)")
    matches = expr.find(self.val)
    if matches:
        return self.error(f"Expected JSON path <{path}> to not exist, but it did.")
    return self

matches_json_schema

matches_json_schema(schema: dict[str, Any]) -> Self

Assert that val conforms to the given JSON Schema.

Parameters:

Name Type Description Default
schema dict[str, Any]

a JSON Schema as a dict.

required

Examples:

Usage:

schema = {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}
assert_that({"name": "Alice"}).matches_json_schema(schema)

Returns:

Name Type Description
AssertionBuilder Self

returns this instance to chain to the next assertion

Raises:

Type Description
AssertionError

if val does not conform to the schema

Source code in assertpy2/json_mixin.py
def matches_json_schema(self, schema: dict[str, Any]) -> Self:
    """Assert that val conforms to the given JSON Schema.

    Args:
        schema: a JSON Schema as a dict.

    Examples:
        Usage:

            schema = {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}
            assert_that({"name": "Alice"}).matches_json_schema(schema)

    Returns:
        AssertionBuilder: returns this instance to chain to the next assertion

    Raises:
        AssertionError: if val does not conform to the schema
    """
    jsonschema_mod = _ensure_jsonschema()
    try:
        require_type(
            schema,
            (dict, bool),
            "a JSON Schema (a dict, or a bool for the trivial schema)",
            subject=argument("schema"),
        )
        jsonschema_mod.validate(self.val, schema)
    except jsonschema_mod.ValidationError as exc:
        # carry the path ourselves: it is the only part of jsonschema's own text the message does
        # not already have, and without it the caught error has to stay in the traceback to say
        # which field failed
        return self.error(
            f"Expected val to match JSON schema, but validation failed at {exc.json_path}: {exc.message}",
            suppress_context=True,
        )
    return self

matches_json_schema_from_file

matches_json_schema_from_file(path: str | Path) -> Self

Assert that val conforms to a JSON Schema loaded from a file.

Parameters:

Name Type Description Default
path str | Path

path to a JSON file containing the schema.

required

Examples:

Usage:

assert_that(data).matches_json_schema_from_file("schemas/order.json")

Returns:

Name Type Description
AssertionBuilder Self

returns this instance to chain to the next assertion

Raises:

Type Description
AssertionError

if val does not conform to the schema

Source code in assertpy2/json_mixin.py
def matches_json_schema_from_file(self, path: str | Path) -> Self:
    """Assert that val conforms to a JSON Schema loaded from a file.

    Args:
        path: path to a JSON file containing the schema.

    Examples:
        Usage:

            assert_that(data).matches_json_schema_from_file("schemas/order.json")

    Returns:
        AssertionBuilder: returns this instance to chain to the next assertion

    Raises:
        AssertionError: if val does not conform to the schema
    """
    require_type(path, (str, PurePath), "a path", subject=argument("path"))
    schema = json.loads(Path(path).read_text(encoding="utf-8"))
    return self.matches_json_schema(schema)

conforms_to_openapi

conforms_to_openapi(
    spec: dict[str, Any],
    path: str,
    method: str,
    *,
    status: str | int | None = None,
    content_type: str = "application/json",
) -> Self

Assert that val conforms to an OpenAPI operation's response-body schema.

val is validated against the schema declared for the application/json response of the method/path operation in spec. This checks only the response body of that one operation - not request bodies, parameters, headers, or the spec as a whole.

OpenAPI 3.0 (its nullable keyword is honoured), 3.1, and Swagger 2.0 (schema declared directly on the response, its x-nullable extension honoured) are all supported. $ref, oneOf/allOf/anyOf, enum, and format all validate with full JSON-Schema semantics, and every violation is reported with its JSON path.

Parameters:

Name Type Description Default
spec dict[str, Any]

a parsed OpenAPI document (dict); loading YAML/JSON is the caller's job.

required
path str

the operation's path template, e.g. "/orders/{id}".

required
method str

the HTTP method, e.g. "get" (case-insensitive).

required
status str | int | None

response status to validate against; defaults to 200, then 201, then default.

None
content_type str

response content type; defaults to "application/json". Swagger 2.0 has no content-type layer, so it is checked against the operation's produces list instead (and skipped when the spec declares none).

'application/json'

Examples:

Usage:

spec = {...}  # your parsed OpenAPI document
assert_that(response.json()).conforms_to_openapi(spec, "/orders/{id}", "get")

Returns:

Name Type Description
AssertionBuilder Self

returns this instance to chain to the next assertion

Raises:

Type Description
AssertionError

if val does not conform to the response schema

ValueError

if the operation, status, or content type is not found in the spec

Source code in assertpy2/json_mixin.py
def conforms_to_openapi(
    self,
    spec: dict[str, Any],
    path: str,
    method: str,
    *,
    status: str | int | None = None,
    content_type: str = "application/json",
) -> Self:
    """Assert that val conforms to an OpenAPI operation's response-body schema.

    val is validated against the schema declared for the ``application/json`` response of the
    ``method``/``path`` operation in ``spec``. This checks only the response body of that one
    operation - not request bodies, parameters, headers, or the spec as a whole.

    OpenAPI 3.0 (its ``nullable`` keyword is honoured), 3.1, and Swagger 2.0 (schema declared directly
    on the response, its ``x-nullable`` extension honoured) are all supported. ``$ref``,
    ``oneOf``/``allOf``/``anyOf``, ``enum``, and ``format`` all validate with full JSON-Schema
    semantics, and every violation is reported with its JSON path.

    Args:
        spec: a parsed OpenAPI document (dict); loading YAML/JSON is the caller's job.
        path: the operation's path template, e.g. ``"/orders/{id}"``.
        method: the HTTP method, e.g. ``"get"`` (case-insensitive).
        status: response status to validate against; defaults to ``200``, then ``201``, then
            ``default``.
        content_type: response content type; defaults to ``"application/json"``. Swagger 2.0 has no
            content-type layer, so it is checked against the operation's ``produces`` list instead
            (and skipped when the spec declares none).

    Examples:
        Usage:

            spec = {...}  # your parsed OpenAPI document
            assert_that(response.json()).conforms_to_openapi(spec, "/orders/{id}", "get")

    Returns:
        AssertionBuilder: returns this instance to chain to the next assertion

    Raises:
        AssertionError: if val does not conform to the response schema
        ValueError: if the operation, status, or content type is not found in the spec
    """
    jsonschema_mod = _ensure_jsonschema()
    import referencing
    from referencing.jsonschema import DRAFT4, DRAFT202012

    spec = _stringify_keys(spec)  # YAML may parse numeric-looking keys (e.g. status 200) as ints
    is_openapi_31 = str(spec.get("openapi", "")).startswith("3.1")
    is_swagger_2 = str(spec.get("swagger", "")).startswith("2")
    status_key, pointer = _openapi_resolve(spec, path, method, status, content_type)
    if is_openapi_31:
        document = spec  # 3.1 is JSON Schema 2020-12 already, no nullable rewrite
    else:
        document = _openapi_nullable_to_null(spec, "x-nullable" if is_swagger_2 else "nullable")
    specification = DRAFT202012 if is_openapi_31 else DRAFT4
    validator_cls = jsonschema_mod.Draft202012Validator if is_openapi_31 else jsonschema_mod.Draft4Validator

    base = "urn:assertpy2-openapi"
    registry = referencing.Registry().with_resource(
        uri=base, resource=referencing.Resource(contents=document, specification=specification)
    )
    validator = validator_cls(
        {"$ref": base + pointer}, registry=registry, format_checker=jsonschema_mod.FormatChecker()
    )
    errors = sorted(validator.iter_errors(self.val), key=lambda error: (error.json_path, str(error.validator)))
    if not errors:
        return self
    entries = [
        DiffEntry(path=error.json_path, actual=error.instance, expected=_openapi_expected(error))
        for error in errors
    ]
    plural = "" if len(entries) == 1 else "s"
    return self.error(
        f"Expected the value to conform to the OpenAPI schema for <{method.upper()} {path}> response"
        f" <{status_key}>, but found {len(entries)} violation{plural}.",
        diff=DiffResult(kind="openapi", entries=entries),
    )