Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,17 @@ from openapi_core import V31RequestValidator
errors = list(V31RequestValidator(spec).iter_errors(request))
```

Some high-level errors wrap detailed schema errors. To access nested schema details:
Validation errors expose structured details directly:

```python
for error in openapi.iter_request_errors(request):
details = getattr(error, "details", {})
print(details.get("message"))
for schema_error in details.get("schema_errors", []):
print(schema_error["message"], schema_error["path"])
```

Some high-level errors wrap detailed schema errors in `__cause__`. You can still access those low-level objects directly:

```python
for error in openapi.iter_request_errors(request):
Expand Down
35 changes: 35 additions & 0 deletions openapi_core/validation/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,46 @@
"""OpenAPI core validation exceptions module"""

from dataclasses import dataclass
from typing import Any

from openapi_core.exceptions import OpenAPIError


def _schema_error_to_dict(schema_error: Exception) -> dict[str, Any]:
message = getattr(schema_error, "message", str(schema_error))
raw_path = getattr(schema_error, "path", ())
try:
path = list(raw_path)
except TypeError:
path = []
return {
"message": message,
"path": path,
}


@dataclass
class ValidationError(OpenAPIError):
@property
def details(self) -> dict[str, Any]:
cause = self.__cause__
schema_errors: list[dict[str, Any]] = []
if cause is not None:
cause_schema_errors = getattr(cause, "schema_errors", None)
if cause_schema_errors is not None:
schema_errors = [
_schema_error_to_dict(schema_error)
for schema_error in cause_schema_errors
]

return {
"message": str(self),
"error_type": self.__class__.__name__,
"cause_type": (
cause.__class__.__name__ if cause is not None else None
),
"schema_errors": schema_errors,
}

def __str__(self) -> str:
return f"{self.__class__.__name__}: {self.__cause__}"
6 changes: 6 additions & 0 deletions tests/integration/unmarshalling/test_request_unmarshaller.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ def test_missing_body(self, request_unmarshaller):

assert len(result.errors) == 1
assert type(result.errors[0]) == MissingRequiredRequestBody
assert result.errors[0].details == {
"message": "Missing required request body",
"error_type": "MissingRequiredRequestBody",
"cause_type": None,
"schema_errors": [],
}
assert result.body is None
assert result.parameters == Parameters(
header={
Expand Down
61 changes: 61 additions & 0 deletions tests/integration/validation/test_strict_json_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,67 @@ def test_request_validator_error_message_includes_cause_details() -> None:
assert "'30' is not of type 'integer'" in error_message


def test_request_validator_error_details_are_structured() -> None:
spec = _spec_schema_path()
validator = V30RequestValidator(spec)

request_json = {
"id": "123e4567-e89b-12d3-a456-426614174000",
"username": "Test User",
"age": "30",
}
request = MockRequest(
"http://example.com",
"post",
"/users",
content_type="application/json",
data=json.dumps(request_json).encode("utf-8"),
)

with pytest.raises(InvalidRequestBody) as exc_info:
validator.validate(request)

details = exc_info.value.details
assert details["error_type"] == "InvalidRequestBody"
assert details["cause_type"] == "InvalidSchemaValue"
assert details["schema_errors"] == [
{
"message": "'30' is not of type 'integer'",
"path": ["age"],
}
]


def test_response_validator_error_details_are_structured() -> None:
spec = _spec_schema_path()
validator = V30ResponseValidator(spec)

request = MockRequest("http://example.com", "get", "/users")
response_json = {
"id": "123e4567-e89b-12d3-a456-426614174000",
"username": "Test User",
"age": "30",
}
response = MockResponse(
json.dumps(response_json).encode("utf-8"),
status_code=200,
content_type="application/json",
)

with pytest.raises(InvalidData) as exc_info:
validator.validate(request, response)

details = exc_info.value.details
assert details["error_type"] == "InvalidData"
assert details["cause_type"] == "InvalidSchemaValue"
assert details["schema_errors"] == [
{
"message": "'30' is not of type 'integer'",
"path": ["age"],
}
]


def test_response_validator_strict_json_nested_types() -> None:
"""Test that nested JSON structures (arrays, objects) remain strict."""
spec_dict = {
Expand Down
Loading