Coverage for src/lilbee/server/validation_format.py: 100%

13 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-11-06 08:17 +0000

1"""Shared rendering of Litestar body-validation failures for wire envelopes.""" 

2 

3from __future__ import annotations 

4 

5import re 

6 

7from litestar.exceptions import ValidationException 

8 

9_BODY_KIND_PATTERN = re.compile(r"^Input should be a valid dictionary or instance of \w+$") 

10BODY_KIND_MESSAGE = "The request body must be a JSON object." 

11 

12 

13def plain_validation(exc: ValidationException) -> ValidationException: 

14 """Replace the message that names an internal model class with a plain one. 

15 

16 A body that is not an object makes pydantic say "a valid dictionary or instance of 

17 <ModelName>", which leaks the name of a class a client never sees. 

18 """ 

19 if isinstance(exc.extra, list): 

20 exc.extra = [ 

21 {**item, "message": BODY_KIND_MESSAGE} 

22 if isinstance(item, dict) and _BODY_KIND_PATTERN.match(str(item.get("message", ""))) 

23 else item 

24 for item in exc.extra 

25 ] 

26 return exc 

27 

28 

29def format_validation(exc: ValidationException) -> str: 

30 """Render a litestar/pydantic ValidationException as one user-facing string. 

31 

32 Litestar wraps pydantic errors as ``{"key": "field_name", "message": "..."}`` 

33 entries on ``exc.extra``; they flatten into a semicolon-joined string so the 

34 error envelope carries the same field names a client expects to see. 

35 """ 

36 items: list[dict[str, str]] = exc.extra if isinstance(exc.extra, list) else [] 

37 parts = [f"{err.get('key') or ''}: {err.get('message', '')}".lstrip(": ") for err in items] 

38 return "; ".join(parts) if parts else str(exc.detail)