From fff3b3244cd9020c1fe5ab0e49831894f46628f8 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Wed, 9 Sep 2026 17:03:28 +0100 Subject: [PATCH 1/2] Validate Model Target error JSON --- src/vws/_json_utils.py | 5 ++ src/vws/exceptions/model_target_exceptions.py | 64 ++++++------------- 2 files changed, 24 insertions(+), 45 deletions(-) diff --git a/src/vws/_json_utils.py b/src/vws/_json_utils.py index a5784d7d1..7d2150d59 100644 --- a/src/vws/_json_utils.py +++ b/src/vws/_json_utils.py @@ -28,6 +28,11 @@ def json_object(*, value: str | bytes | bytearray) -> dict[str, object]: return _validated_object(value=loaded) +def object_field(*, value: dict[str, object], name: str) -> dict[str, object]: + """Return a required JSON object field.""" + return _validated_object(value=value[name]) + + def string_value(*, value: object, name: str) -> str: """Return a JSON value after validating that it is a string.""" if not isinstance(value, str): diff --git a/src/vws/exceptions/model_target_exceptions.py b/src/vws/exceptions/model_target_exceptions.py index 0031d35b3..72cc251e2 100644 --- a/src/vws/exceptions/model_target_exceptions.py +++ b/src/vws/exceptions/model_target_exceptions.py @@ -5,52 +5,32 @@ """ import json -from typing import Any from beartype import beartype +from vws._json_utils import ( + json_object, + object_field, + object_list_field, + string_field, +) from vws.reports import ModelTargetGenerationDetail from vws.response import Response @beartype -def _is_json_object(*, value: object) -> bool: - """Get whether a decoded JSON value is an object. - - Args: - value: A decoded JSON value. - - Returns: - Whether the value is a JSON object. - """ - return isinstance(value, dict) - - -@beartype -def _json_object(*, value: str) -> dict[str, Any]: # pyrefly: ignore [explicit-any] - """Get a JSON object from a string. - - Args: - value: A string which may be a JSON object. - - Returns: - The JSON object, or an empty dictionary if the string is not a - JSON object. +def _json_object(*, value: str) -> dict[str, object]: + """Return a decoded JSON object, or an empty object for invalid + input. """ try: - loaded: Any = json.loads(s=value) # pyrefly: ignore [explicit-any] - except json.JSONDecodeError: + return json_object(value=value) + except json.JSONDecodeError, TypeError: return {} - if not _is_json_object(value=loaded): - return {} - - json_object: dict[str, Any] = loaded # pyrefly: ignore [explicit-any] - return json_object - @beartype -def _error_dict(*, response: Response) -> dict[str, Any]: # pyrefly: ignore [explicit-any] +def _error_dict(*, response: Response) -> dict[str, object]: """Get the error object of a Model Target Web API error response. Args: @@ -62,17 +42,12 @@ def _error_dict(*, response: Response) -> dict[str, Any]: # pyrefly: ignore [ex balancer in front of Vuforia, are not shaped like Model Target Web API errors. """ - body = _json_object(value=response.text) - if "error" not in body: - return {} - - error: Any = body["error"] # pyrefly: ignore [explicit-any] - if not _is_json_object(value=error): + try: + body = _json_object(value=response.text) + return object_field(value=body, name="error") + except KeyError, TypeError: return {} - error_dict: dict[str, Any] = error # pyrefly: ignore [explicit-any] - return error_dict - @beartype class ModelTargetError(Exception): @@ -122,11 +97,10 @@ def details(self) -> list[ModelTargetGenerationDetail]: return [ ModelTargetGenerationDetail( - code=detail["code"], # pyrefly: ignore [unknown-argument-type] - # pyrefly: ignore [unknown-argument-type] - message=detail["message"], + code=string_field(value=detail_object, name="code"), + message=string_field(value=detail_object, name="message"), ) - for detail in error["details"] + for detail_object in object_list_field(value=error, name="details") ] From 606f7b117d9ee9a23948d05b5505381034dad1c1 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Wed, 9 Sep 2026 17:45:18 +0100 Subject: [PATCH 2/2] Validate decoded JSON recursively --- src/vws/_json_utils.py | 30 ++++++++++++------- src/vws/exceptions/model_target_exceptions.py | 5 ++-- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/vws/_json_utils.py b/src/vws/_json_utils.py index 7d2150d59..aa60dbc8d 100644 --- a/src/vws/_json_utils.py +++ b/src/vws/_json_utils.py @@ -3,18 +3,24 @@ import json from typing import TypeGuard +from beartype.door import TypeHint -def _is_json_object(value: object, /) -> TypeGuard[dict[str, object]]: +type JSONValue = ( + bool | int | float | str | list[JSONValue] | dict[str, JSONValue] | None +) + + +def _is_json_object(value: object, /) -> TypeGuard[dict[str, JSONValue]]: """Return whether a decoded JSON value is an object.""" - return isinstance(value, dict) + return TypeHint(hint=dict[str, JSONValue]).is_bearable(obj=value) -def _is_object_list(value: object, /) -> TypeGuard[list[object]]: +def _is_object_list(value: object, /) -> TypeGuard[list[JSONValue]]: """Return whether a decoded JSON value is an array.""" - return isinstance(value, list) + return TypeHint(hint=list[JSONValue]).is_bearable(obj=value) -def _validated_object(*, value: object) -> dict[str, object]: +def _validated_object(*, value: object) -> dict[str, JSONValue]: """Return a decoded JSON object.""" if not _is_json_object(value): msg = "Expected a JSON object." @@ -22,13 +28,15 @@ def _validated_object(*, value: object) -> dict[str, object]: return value -def json_object(*, value: str | bytes | bytearray) -> dict[str, object]: +def json_object(*, value: str | bytes | bytearray) -> dict[str, JSONValue]: """Decode and validate a JSON object.""" loaded: object = json.loads(s=value) return _validated_object(value=loaded) -def object_field(*, value: dict[str, object], name: str) -> dict[str, object]: +def object_field( + *, value: dict[str, JSONValue], name: str +) -> dict[str, JSONValue]: """Return a required JSON object field.""" return _validated_object(value=value[name]) @@ -41,14 +49,14 @@ def string_value(*, value: object, name: str) -> str: return value -def string_field(*, value: dict[str, object], name: str) -> str: +def string_field(*, value: dict[str, JSONValue], name: str) -> str: """Return a required string field from a JSON object.""" return string_value(value=value[name], name=name) def string_list_field( *, - value: dict[str, object], + value: dict[str, JSONValue], name: str, ) -> list[str]: """Return a required list of strings from a JSON object.""" @@ -63,9 +71,9 @@ def string_list_field( def object_list_field( *, - value: dict[str, object], + value: dict[str, JSONValue], name: str, -) -> list[dict[str, object]]: +) -> list[dict[str, JSONValue]]: """Return a required list of JSON objects.""" items = value[name] if not _is_object_list(items): diff --git a/src/vws/exceptions/model_target_exceptions.py b/src/vws/exceptions/model_target_exceptions.py index 72cc251e2..c810b55de 100644 --- a/src/vws/exceptions/model_target_exceptions.py +++ b/src/vws/exceptions/model_target_exceptions.py @@ -9,6 +9,7 @@ from beartype import beartype from vws._json_utils import ( + JSONValue, json_object, object_field, object_list_field, @@ -19,7 +20,7 @@ @beartype -def _json_object(*, value: str) -> dict[str, object]: +def _json_object(*, value: str) -> dict[str, JSONValue]: """Return a decoded JSON object, or an empty object for invalid input. """ @@ -30,7 +31,7 @@ def _json_object(*, value: str) -> dict[str, object]: @beartype -def _error_dict(*, response: Response) -> dict[str, object]: +def _error_dict(*, response: Response) -> dict[str, JSONValue]: """Get the error object of a Model Target Web API error response. Args: