3

We are using fastapi and pydantic to excellent effect. However, there are some validations that we perform that can not (should not?) be done in a pydantic validator. (Mostly database/world-state kinds of validations)

Examples:

  • When creating a User with a username, the pydantic model has validations for length and allowed characters, but a uniqueness check requires a database call, and so should not be done in a pydantic validator.
    • Further, this should be enforced by the database, so it can be done atomically (create and check for error, rather than check if name exists and then try to create).
  • Creating a Document object with several "related to" links that mention other documents by id
    • These are foreign key relationships in the database, so the API endpoint must check if the linked ids are legitimate ids that are in use. (Again, should be done at the database layer)

The end result:

  • The API endpoint itself implements a number of validations beyond those in the pydantic model (acceptable/expected)
  • These validations are applied one at a time, so a user can only see one error message, fix it, and then go on to see the next one. (undesirable)
  • It is difficult to return standardized error messages. (undesirable)
    • pydantic's error messages are excellent: structured, describe individual fields, and can describe multiple errors at once.
{
  "detail": [
    {
      "loc": [
        "body",
        "username"
      ],
      "msg": "Username must be at least 5 characters long. Username cannot contain '-' character.",
      "type": "value_error"
    }
  ]
}

How can I:

  • Return error messages with this same structure? Ideal solution would be something like:
try:
    db.create_user(user)
except UserAlreadyExists:
    raise pydantic.<something>(User.username, "This username is already in use.")
  • Aggregate a number of errors at once: eg. Document name is not unique and "related to" links (a list) item #3 is not found.

I know that I can (and have) built error handling to reflect this same structure, and even tried to make it reasonably dynamic to handle different models/fields, but it is a lot of effort. If there were something provided by pydantic directly, that would be more convenient.

I did come across https://stackoverflow.com/a/76601052/15963311 which mentions ErrorWrapper from pydantic. At a glance, it seems to do what I want, but once I discovered that it is deprecated in pydantic V2, I didn't bother investigating it thoroughly.

2
  • Hi! Do you have any results? I have the same question. Commented May 29, 2024 at 11:14
  • Aside from self maintaining a similar error structure, not really. Commented May 29, 2024 at 11:35

1 Answer 1

1

I have a small workaround for our problem.

# service.py

from pydantic import ValidationError
from pydantic_core import InitErrorDetails

from sqlalchemy.orm import Session

def create_user(db_session: Session, user_in: UserCreate) -> User:
    user = get_user_by_email(db_session=db_session, email=user_in.email)
    if user:
        raise ValidationError.from_exception_data(
            title="error title",
            line_errors=[
                InitErrorDetails(
                    type_error="value_error",
                    loc=("email",),
                    input=user_in.email,
                    ctx={},
                ),
            ]
        )
    ...

With InitErrorDetails you can collect errors and then raise them all at once. https://docs.pydantic.dev/latest/api/pydantic_core/#pydantic_core.ValidationError https://docs.pydantic.dev/latest/api/pydantic_core/#pydantic_core.InitErrorDetails

But if we throw an error in this format, we'll get 500 status code. We need one more step to throw errors outside pydantic models.

# exceptions.py

from pydantic import ValidationError

from fastapi.responses import JSONResponse

from .main import app

@app.exception_handler(ValidationError)
def pydantic_exception_outside_model(request, exc):
    return JSONResponse(
        status_code=422,
        content={
            "detail": exc.errors()  # contains pydantic style errors
        }
    )

https://fastapi.tiangolo.com/tutorial/handling-errors/#override-request-validation-exceptions

May be it'll be better to subclass pydantic ValidationError with your own class to avoid possible errors and collisions.

UPD: ValidationError class marked as final, so we can't subclass it.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.