Home > OS >  return list of validation errors on field in pydantic
return list of validation errors on field in pydantic

Time:04-18

Is it possible to return a list of validation errors for a specific field, without having separate validators for the same field? It would be nice to get all errors back in 1 shot for the field, instead of having to get separate responses back for each failed validation.

@validator('password')
def check_password(cls, value):
    password = value.get_secret_value()

    failed = []

    min_length = 8
    if len(password) < min_length:
        failed.append('Password must be at least 8 characters long.')
        return value

    if not any(character.islower() for character in password):
        failed.append('Password missing lower case')
        return value

   if len(failed) > 0:
      raise ValueError(failed)

Output:

{
    "detail": [
        {
            "loc": [
                "body",
                "password"
            ],
            "msg": "['Password must be at least 8 characters long.', 'Password missing lower case']",
            "type": "assertion_error"
        }
    ]
}

However, "msg" comes as a string list. Can we get that as a list?

CodePudding user response:

I don't think that's possible.

If you check out the class delcaration ValidationError In the repository for pydantic, you see how the msg prop is generated using the method error_dict

In error_dict msg is generated using the following code :


 type_ = get_exc_type(exc.__class__)
    msg_template = config.error_msg_templates.get(type_) or getattr(exc, 'msg_template', None)
    ctx = exc.__dict__
    if msg_template:
        msg = msg_template.format(**ctx)
    else:
        msg = str(exc)

Which will always return a string, no matter what you pass to your ValueError constructor.

Furthermore, splitting your function into multiple validators doesn't seem to work either, as pydantic will only report the first failing validator


from pydantic import BaseModel, validator


class TestModel(BaseModel):
    password: str

    @validator("password")
    def is_lower_case(cls, value):
        if not value.islower():
            raise ValueError("Must be lower")
        return value
        
    @validator("password")
    def is_long_enough(cls, value):
        if len(value) < 3:
            raise ValueError("Too short")
        return value
        

Output:


>>> from test_model import TestModel
>>> TestModel(password="Te")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "pydantic/main.py", line 331, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for TestModel
password
  Must be lower (type=value_error)

CodePudding user response:

Here is my "caveman approach" for this problem - unwrap the string list with regex:

import re
from pydantic import BaseModel, ValidationError, validator

class UserModel(BaseModel):

    username: str
    password: str

    @validator("password")
    def passwords_match(cls, v):
        errors = []
        if "7" in v:
            errors.append("Illegal password")
        if len(v) < 4:
            errors.append("too short")
        if errors:
            raise ValueError(errors)

        return v

try:
    UserModel(
        username='scolvin',
        password='gg7'
    )
except ValidationError as e:
    for error in e.errors():
        error_msg = error["msg"]
        L = [m[0][1:-1] for m in re.finditer("'[^']*'", error_msg)]
        error["msg"] = L
    print(e.json())

It contains a complete reproducible minimal example and solution. It is indeed not that elegant, but might just be what solves your problem.

CodePudding user response:

Decided to add a middleware to FastAPI as a low touch approach to solving this for my use case. I do not want to modify / manage custom installations of pydantic, so adding a middleware to my API seems like a better alternative, for now?

@app.exception_handler(RequestValidationError)
def validation_exception_handler(request, exc):
    errors = []

    for each in exc.errors():
        error = {"message" if key == "msg" else key: ast.literal_eval(value) if value[0] == '[' and value[-1] == ']' else value for key, value in each.items()}
        errors.append(error)

    return JSONResponse({"detail": errors}, status_code = 422)

Above logic is renaming the "msg" key to "message", and converting list representations in a string to a list.

  • Related