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)
    # handed a scalar, jsonpath answers 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, logger=self.logger)
    return self.builder([match.value for match in matches], self.description, self.kind, logger=self.logger)

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)
    # handed a scalar, jsonpath answers 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.", expected=path)
    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)
    # handed a scalar, jsonpath answers 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:
        # the path is the only part of jsonschema's text the message does not already have
        return self.error(
            f"Expected val to match JSON schema, but validation failed at {exc.json_path}: {exc.message}",
            suppress_context=True,
            expected=schema,
        )
    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

    normalized = _stringify_keys(spec)  # YAML may parse numeric-looking keys (e.g. status 200) as ints
    version = str(normalized.get("openapi", ""))
    is_swagger_2 = _SWAGGER_2.fullmatch(str(normalized.get("swagger", ""))) is not None
    # matched whole, not by prefix: "3.10.0" starts with "3.1", and "3.1.garbage" is no version at all
    read = _OPENAPI_VERSION.fullmatch(version)
    if version and read is None:
        # read as 3.0 an unknown version validates against Draft 4, which passes anything it cannot spell
        raise ValueError(f"openapi version <{version}> is not one this can validate (3.0, 3.1 and 3.2 are)")
    # 3.2 keeps 3.1's dialect: the schema object is JSON Schema 2020-12 in both
    is_openapi_31 = read is not None and read.group(1) in ("1", "2")
    status_key, pointer = _openapi_resolve(normalized, path, method, status, content_type)
    if is_openapi_31:
        document = normalized  # 3.1 is JSON Schema 2020-12 already, no nullable rewrite
    else:
        document = _openapi_nullable_to_null(normalized, "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),
    )