Skip to content

Guide pratique : gérer les erreurs de validation ​

Comment retourner des erreurs de validation de domaine au client en utilisant ValidationException et ValidationError depuis nene2.validation.


1. Définir ValidationCode comme un StrEnum ​

Le framework ne définit pas de codes d'erreur standard. Définissez-les par projet avec un StrEnum.

python
from enum import StrEnum

class ValidationCode(StrEnum):
    REQUIRED = "required"
    INVALID_FORMAT = "invalid_format"
    TOO_SHORT = "too_short"
    TOO_LONG = "too_long"
    ALREADY_EXISTS = "already_exists"
    OUT_OF_RANGE = "out_of_range"

Utiliser un StrEnum :

  • peut être passé directement au constructeur de ValidationError (typé de façon sûre)
  • se sérialise en sa valeur de chaîne dans JSON
  • bénéficie de la complétion IDE et de l'analyse statique

2. Valider plusieurs champs ​

Accumulez les erreurs dans une liste et levez une seule ValidationException.

python
from nene2.validation import ValidationError, ValidationException

def validate_registration(username: str, email: str, age: int) -> None:
    errors: list[ValidationError] = []

    if len(username) < 3:
        errors.append(ValidationError("username", "At least 3 characters required", ValidationCode.TOO_SHORT))
    if "@" not in email:
        errors.append(ValidationError("email", "Enter a valid email address", ValidationCode.INVALID_FORMAT))
    if age < 0 or age > 150:
        errors.append(ValidationError("age", "Age must be between 0 and 150", ValidationCode.OUT_OF_RANGE))

    if errors:
        raise ValidationException(errors)

Exemple de réponse (422) :

json
{
  "type": "https://example.com/problems/validation-failed",
  "title": "Validation Failed",
  "status": 422,
  "errors": [
    {"field": "username", "message": "At least 3 characters required", "code": "too_short"},
    {"field": "email",    "message": "Enter a valid email address", "code": "invalid_format"}
  ]
}

3. Valider un seul champ ​

ValidationException.single() lève en une seule ligne.

python
from nene2.validation import ValidationException

raise ValidationException.single("email", "Email is required", ValidationCode.REQUIRED)

4. Chemins pour les champs imbriqués ​

Passez le chemin d'un champ imbriqué comme une chaîne séparée par des points (il n'y a pas de helper de normalisation).

python
ValidationError("address.city", "Required", ValidationCode.REQUIRED)
ValidationError("items.0.quantity", "Enter 1 or more", ValidationCode.OUT_OF_RANGE)

5. Une erreur @model_validator a le champ "request" ​

Quand vous levez ValueError(...) dans un @model_validator(mode="after") Pydantic, loc est un tuple vide. La conversion ValidationException de nene2 le mappe vers field: "request".

python
class RegisterBody(BaseModel):
    password: str
    password_confirm: str

    @model_validator(mode="after")
    def passwords_match(self) -> Self:
        if self.password != self.password_confirm:
            raise ValueError("Passwords do not match")
        return self

Exemple de réponse (422) :

json
{
  "errors": [
    {
      "field": "request",
      "message": "Value error, Passwords do not match",
      "code": "value_error"
    }
  ]
}

Côté frontend, traitez field: "request" comme une erreur pour tout le formulaire, et incluez le nom du champ pertinent dans le texte du message d'erreur.


6. Intégration avec ErrorHandlerMiddleware ​

Ajouter nene2.middleware.ErrorHandlerMiddleware à l'application convertit une ValidationException en réponse 422 Problem Details automatiquement.

python
from nene2.middleware import ErrorHandlerMiddleware

app = FastAPI()
app.add_middleware(ErrorHandlerMiddleware)

7. Retourner 400 pour les échecs de décodage d'entrées externes ​

Avec Query() / Body() de Pydantic, un échec de conversion de type devient un 422. Mais pour les choses que vous "recevez comme une chaîne et décodez vous-même" — curseurs (Base64), tokens, etc. — si vous n'attrapez pas l'exception de décodage, ErrorHandlerMiddleware la retourne comme un 500.

python
from fastapi.responses import JSONResponse

@app.get("/posts")
def list_posts(after: str | None = Query(None)) -> JSONResponse:
    if after is not None:
        try:
            cursor_id = _decode_cursor(after)  # p. ex. base64.urlsafe_b64decode
        except Exception:
            return JSONResponse({"detail": "Invalid cursor"}, status_code=400)
        # continuer en utilisant cursor_id

Capturez les échecs de décodage comme binascii.Error ensemble via ValueError / Exception et retournez 400 Bad Request — c'est une erreur d'entrée utilisateur, pas une erreur serveur.

Publié sous la licence MIT.