Skip to content

Commit 7e98eda

Browse files
committed
Add validation handling
1 parent 8f616ae commit 7e98eda

File tree

5 files changed

+41
-12
lines changed

5 files changed

+41
-12
lines changed

conduit/api/schemas/requests/comment.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from pydantic import BaseModel
1+
from pydantic import BaseModel, Field
22

33
from conduit.domain.dtos.comment import CreateCommentDTO
44

55

66
class CreateCommentData(BaseModel):
7-
body: str
7+
body: str = Field(..., min_length=1)
88

99

1010
class CreateCommentRequest(BaseModel):

conduit/api/schemas/requests/user.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
from pydantic import BaseModel, Field
1+
from pydantic import BaseModel, EmailStr, Field
22

33
from conduit.domain.dtos.user import CreateUserDTO, LoginUserDTO, UpdateUserDTO
44

55

66
class UserRegistrationData(BaseModel):
7-
email: str
8-
password: str
9-
username: str
7+
email: EmailStr
8+
password: str = Field(..., min_length=8)
9+
username: str = Field(..., min_length=3)
1010

1111

1212
class UserLoginData(BaseModel):
13-
email: str
14-
password: str
13+
email: EmailStr
14+
password: str = Field(..., min_length=8)
1515

1616

1717
class UserUpdateData(BaseModel):

conduit/core/exceptions.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
from typing import Any
2+
13
from fastapi import FastAPI
24
from fastapi.exceptions import RequestValidationError
35
from starlette.exceptions import HTTPException
46
from starlette.requests import Request
57
from starlette.responses import JSONResponse
68

9+
from conduit.core.utils.errors import format_errors
10+
711

812
class BaseInternalException(Exception):
913
"""
@@ -12,12 +16,13 @@ class BaseInternalException(Exception):
1216

1317
_status_code = 0
1418
_message = ""
19+
_errors: dict = {}
1520

1621
def __init__(
1722
self,
1823
status_code: int | None = None,
1924
message: str | None = None,
20-
errors: list[str] | None = None,
25+
errors: dict[str, dict[Any, Any]] | None = None,
2126
) -> None:
2227
self.status_code = status_code
2328
self.message = message
@@ -29,6 +34,9 @@ def get_status_code(self) -> int:
2934
def get_message(self) -> str:
3035
return self.message or self._message
3136

37+
def get_errors(self) -> dict[str, dict[Any, Any]]:
38+
return self.errors or self._errors
39+
3240
@classmethod
3341
def get_response(cls) -> JSONResponse:
3442
return JSONResponse(
@@ -38,6 +46,7 @@ def get_response(cls) -> JSONResponse:
3846
"status_code": cls._status_code,
3947
"type": cls.__name__,
4048
"message": cls._message,
49+
"errors": cls._errors,
4150
},
4251
)
4352

@@ -96,20 +105,26 @@ class EmailAlreadyTakenException(BaseInternalException):
96105

97106
_status_code = 400
98107
_message = "User with this email already exists."
108+
_errors = {"email": ["user with this email already exists."]}
99109

100110

101111
class UserNameAlreadyTakenException(BaseInternalException):
102112
"""Exception raised when username was found in database while registration."""
103113

104114
_status_code = 400
105115
_message = "User with this username already exists."
116+
_errors = {"username": ["user with this username already exists."]}
106117

107118

108119
class IncorrectLoginInputException(BaseInternalException):
109120
"""Exception raised when email or password was incorrect while login."""
110121

111122
_status_code = 400
112123
_message = "Incorrect email or password."
124+
_errors = {
125+
"email": ["incorrect email or password."],
126+
"password": ["incorrect email or password."],
127+
}
113128

114129

115130
class IncorrectJWTTokenException(BaseInternalException):
@@ -170,6 +185,7 @@ async def _exception_handler(
170185
"status_code": exc.get_status_code(),
171186
"type": type(exc).__name__,
172187
"message": exc.get_message(),
188+
"errors": exc.get_errors(),
173189
},
174190
)
175191

@@ -190,7 +206,7 @@ async def _exception_handler(
190206
"status_code": 422,
191207
"type": "RequestValidationError",
192208
"message": "Schema validation error",
193-
"errors": exc.errors(),
209+
"errors": format_errors(errors=exc.errors()),
194210
},
195211
)
196212

conduit/core/utils/errors.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from collections.abc import Awaitable
1+
from collections import defaultdict
2+
from collections.abc import Awaitable, Sequence
23
from typing import Any
34

45

@@ -10,3 +11,15 @@ async def get_or_raise(awaitable: Awaitable, exception: Exception) -> Any:
1011
if not result:
1112
raise exception
1213
return result
14+
15+
16+
def format_errors(errors: Sequence[Any]) -> dict[str, list[str]]:
17+
"""
18+
Format errors from pydantic validation errors.
19+
"""
20+
result: defaultdict[str, list[str]] = defaultdict(list)
21+
for error in errors:
22+
field = error["loc"][-1]
23+
message = error.get("ctx", {}).get("reason") or error["msg"]
24+
result[field].append(message.lower())
25+
return dict(result)

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ fastapi==0.115.4
44
greenlet==3.1.1
55
httpx==0.27.2
66
passlib[bcrypt]==1.7.4
7-
pydantic==2.9.2
87
pydantic-settings==2.6.1
8+
pydantic[email]==2.9.2
99
pyjwt==2.9.0
1010
pytest==8.3.3
1111
pytest-asyncio==0.24.0

0 commit comments

Comments
 (0)