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 assertpy/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 assertpy/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 assertpy/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 assertpy/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 assertpy/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",
    strict_nullable: bool = False,
) -> 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 and enum all validate with full JSON-Schema semantics, and every violation is reported with its JSON path.

Under OpenAPI 3.0, nullable: true adds null to the type beside it and leaves an enum as written, so null passes an enum only where the enum lists it. A property marked writeOnly: true is not asked for by required, since val is a response.

A multipleOf divides the numbers as they are written, so 19.99 is a multiple of 0.01, which it is not as two floats divided. A float is read as the decimal it prints as: digits a float does not keep are gone before val is handed in, and a decimal.Decimal keeps them.

A format is checked where there is a check for it: date, time, date-time, email, ipv4, ipv6, uuid, regex, uri, hostname, duration, and OpenAPI's int32 and int64. A time is RFC 3339's, with its offset: 10:00:00Z, and not 10:00:00. A hostname under OpenAPI 3.1 has its xn-- labels held to IDNA by the idna package. An email is the mailbox of RFC 5321, and a duration has no fraction: PT0.5S is none. The others jsonschema checks only beside a package of their own, uri-reference, iri, json-pointer and uri-template among them, are checked where that package is installed (jsonschema[format-nongpl] brings them all), and pass unchecked where it is not.

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 the range 2XX, then default. A status is looked up by its own code, then by its range (404 under 4XX), then under default. Swagger 2.0 has no ranges.

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'
strict_nullable bool

hold OpenAPI 3.0's nullable to its text, where it adds null to the type beside it and does nothing with no type there. By default nullable: true beside a $ref, an allOf, an anyOf or a oneOf allows null as well, which is how a nullable reference is usually written. Swagger 2.0's x-nullable and OpenAPI 3.1 are read the same either way.

False

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, a schema of OpenAPI 3.0 or Swagger 2.0 holds a keyword of later JSON Schema, which would be passed over, or a multipleOf that val is held to is no number above zero

Source code in assertpy/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",
    strict_nullable: bool = False,
) -> 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`` and ``enum`` all validate with full JSON-Schema semantics, and every
    violation is reported with its JSON path.

    Under OpenAPI 3.0, ``nullable: true`` adds ``null`` to the ``type`` beside it and leaves an ``enum``
    as written, so ``null`` passes an enum only where the enum lists it.  A property marked
    ``writeOnly: true`` is not asked for by ``required``, since val is a response.

    A ``multipleOf`` divides the numbers as they are written, so ``19.99`` is a multiple of ``0.01``,
    which it is not as two floats divided.  A float is read as the decimal it prints as: digits a float
    does not keep are gone before val is handed in, and a `decimal.Decimal` keeps them.

    A ``format`` is checked where there is a check for it: ``date``, ``time``, ``date-time``, ``email``,
    ``ipv4``, ``ipv6``, ``uuid``, ``regex``, ``uri``, ``hostname``, ``duration``, and OpenAPI's ``int32``
    and ``int64``.  A ``time`` is RFC 3339's, with its offset: ``10:00:00Z``, and not ``10:00:00``.  A
    ``hostname`` under OpenAPI 3.1 has its ``xn--`` labels held to IDNA by the ``idna`` package.  An
    ``email`` is the mailbox of RFC 5321, and a ``duration`` has no fraction: ``PT0.5S`` is none.  The
    others jsonschema checks only beside a package of their own, ``uri-reference``,
    ``iri``, ``json-pointer`` and ``uri-template`` among them, are checked where that package is
    installed (``jsonschema[format-nongpl]`` brings them all), and pass unchecked where it is not.

    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 the range
            ``2XX``, then ``default``.  A status is looked up by its own code, then by its range
            (``404`` under ``4XX``), then under ``default``.  Swagger 2.0 has no ranges.
        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).
        strict_nullable: hold OpenAPI 3.0's ``nullable`` to its text, where it adds ``null`` to the
            ``type`` beside it and does nothing with no ``type`` there.  By default ``nullable: true``
            beside a ``$ref``, an ``allOf``, an ``anyOf`` or a ``oneOf`` allows ``null`` as well, which is
            how a nullable reference is usually written.  Swagger 2.0's ``x-nullable`` and OpenAPI 3.1 are
            read the same either way.

    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, a schema of
            OpenAPI 3.0 or Swagger 2.0 holds a keyword of later JSON Schema, which would be passed over,
            or a ``multipleOf`` that val is held to is no number above zero
    """
    part = _part_read(spec, path, method)
    over = None
    if part is not None:
        over = _validator_over(
            part, path, method, status, content_type, whole=False, strict_nullable=strict_nullable
        )
    if over is None:
        # YAML may parse numeric-looking keys (e.g. status 200) as ints, so the copy has them as text
        over = _validator_over(
            _stringify_keys(spec), path, method, status, content_type, whole=True, strict_nullable=strict_nullable
        )
    status_key, validator = cast("tuple[str, Any]", over)
    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),
    )