简体   繁体   中英

return list of validation errors on field in pydantic

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?

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)

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.

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.

You can use ast.literal_eval on msg field value.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM