👽️ add optional pydantic support

Pydantic is now an optional dependency, and the middleware provides
proper import guards and fallback behavior if Pydantic is not installed.
Updated docs to clarify installation and configuration steps.
This commit is contained in:
Joe Kaufeld 2025-10-11 20:14:46 -04:00
parent 223c7f3cc6
commit ced11ac2da
3 changed files with 44 additions and 6 deletions

View file

@ -9,6 +9,9 @@ app = SpiderwebRouter(
```
When working with form data, you may not want to always have to perform your own validation on the incoming data. Spiderweb gives you a way out of the box to perform this validation using Pydantic.
> [!WARNING]
> Pydantic is not installed by default. Install it with `pip install pydantic` or `pip install spiderweb[pydantic]`.
Let's assume that we have a form view that looks like this:
```python
@ -54,4 +57,4 @@ The Pydantic middleware will automatically detect that the model that you want t
If the validation fails, the middleware will call `on_error`, which by default will return a 400 with a list of the broken fields. You may not want this behavior, so the easiest way to address it is to subclass PydanticMiddleware with your own version and override `on_error` to do whatever you'd like.
If validation succeeds, the data from the validator will appear on the request object under `request.validated_data` — to access it, just call `.dict()` on the validated data.
If validation succeeds, the data from the validator will appear on the request object under `request.validated_data` — to access it, just call `.dict()` on the validated data.

View file

@ -34,7 +34,10 @@ peewee = "^3.17.6"
jinja2 = "^3.1.4"
cryptography = "^43.0.0"
email-validator = "^2.2.0"
pydantic = "^2.8.2"
[tool.poetry.extras]
# Optional dependencies groups
pydantic = ["pydantic>=2.8.2,<3"]
[tool.poetry.group.dev.dependencies]
ruff = "^0.5.5"
@ -85,4 +88,4 @@ exclude_also = [
"if TYPE_CHECKING:",
]
ignore_errors = true
ignore_errors = true

View file

@ -1,8 +1,26 @@
import inspect
from typing import get_type_hints
from pydantic import BaseModel
from pydantic_core._pydantic_core import ValidationError
try: # pragma: no cover - import guard
from pydantic import BaseModel # type: ignore
from pydantic_core._pydantic_core import ValidationError # type: ignore
PYDANTIC_AVAILABLE = True
except Exception: # pragma: no cover - executed only when pydantic isn't installed
PYDANTIC_AVAILABLE = False
class BaseModel: # minimal stub to allow module import without pydantic
@classmethod
def parse_obj(cls, *args, **kwargs): # noqa: D401 - simple shim
raise RuntimeError(
"Pydantic is not installed. Install with 'pip install"
" spiderweb-framework[pydantic]' or 'pip install pydantic'"
" to use PydanticMiddleware."
)
class ValidationError(Exception): # simple stand-in so type hints resolve
def errors(self): # match pydantic's ValidationError API used below
return []
from spiderweb import SpiderwebMiddleware
from spiderweb.request import Request
from spiderweb.response import JsonResponse
@ -19,6 +37,12 @@ class PydanticMiddleware(SpiderwebMiddleware):
def process_request(self, request):
if not request.method == "POST":
return
if not PYDANTIC_AVAILABLE:
raise RuntimeError(
"Pydantic is not installed. Install with 'pip install"
" spiderweb-framework[pydantic]' or 'pip install pydantic'"
" to use PydanticMiddleware."
)
types = get_type_hints(request.handler)
# we don't know what the user named the request object, but
# we know that it's first in the list, and it's always an arg.
@ -34,7 +58,15 @@ class PydanticMiddleware(SpiderwebMiddleware):
# Separated out into its own method so that it can be overridden
errors = e.errors()
error_dict = {"message": "Validation error", "errors": []}
# [{'type': 'missing', 'loc': ('comment',), 'msg': 'Field required', 'input': {'email': 'a@a.com'}, 'url': 'https://errors.pydantic.dev/2.8/v/missing'}]
# [
# {
# 'type': 'missing',
# 'loc': ('comment',),
# 'msg': 'Field required',
# 'input': {'email': 'a@a.com'},
# 'url': 'https://errors.pydantic.dev/2.8/v/missing'
# }
# ]
for error in errors:
field = error["loc"][0]
msg = error["msg"]