4

Is there any way to change the default response from Pydantic so that "msg" is "message"?

{
    "detail": [
        {
            "loc": [
                "body",
                "password"
            ],
            "msg": "Password should at least 8 characters long.",
            "type": "value_error"
        }
    ]
}
Chris
  • 18,724
  • 6
  • 46
  • 80
dataviews
  • 2,466
  • 7
  • 31
  • 64
  • Maybe try this https://pydantic-docs.helpmanual.io/usage/model_config/ – Willow Apr 17 '22 at 16:55
  • @Willow I don't think this changes the "msg" key – dataviews Apr 17 '22 at 17:00
  • 2
    What you call "the default response from pydantic" in your question seems to be a `ValidationError`, but you show what seems to be JSON. I think your question should be improved because it depends on how you come up with that JSON from a `ValidationError`. Check [How do I ask a good question?](https://stackoverflow.com/help/how-to-ask). – Hernán Alarcón Apr 20 '22 at 03:53
  • 2
    Are you using FastAPI? That kind of response is what FastAPI, by default, returns as a response when the request results in a Pydantic ValidationError: https://fastapi.tiangolo.com/tutorial/handling-errors/#override-request-validation-exceptions – Gino Mempin Apr 25 '22 at 11:10
  • 1
    @GinoMempin Yes I am using fastapi – dataviews Apr 25 '22 at 13:17
  • @GinoMempin Is it possible to capture multiple validator error messages in one go when using fastapi and pydantic? Please see my other question – dataviews Apr 25 '22 at 13:47
  • @Chris no, in my case I have multiple requirements for the password field, for example it must be greater than 8 characters, must contain 1 upper case, 1 lower case. If I use a custom validator(), then I have not figured out how to test all conditions against the provided value in the request and return 1 list of errors like fastapi does if fields are missing in the request. Instead, Python is raising an error on any failed condition and immediately returning the ValueError exception. – dataviews Apr 25 '22 at 14:31
  • @Chris this is what I'm really after, to solve: https://stackoverflow.com/questions/71905671/pydantic-cycle-through-all-validators-even-if-one-fails-fastapi/ – dataviews Apr 25 '22 at 14:32

4 Answers4

2

That looks like a JSON response and Pydantic by itself does not give out a JSON response for ValidationError's. It should just be a regular raise-d Exception like this:

In [2]: from pydantic import BaseModel, constr

In [3]: class Credentials(BaseModel):
   ...:     password: constr(min_length=8)
   ...: 

In [4]: Credentials(password="xyz")
---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
Input In [4], in <cell line: 1>()
----> 1 Credentials(password="xyz")

...

ValidationError: 1 validation error for Credentials
password
  ensure this value has at least 8 characters (type=value_error.any_str.min_length; limit_value=8)

I think you are using FastAPI (which has Pydantic integration) and this JSON response is actually FastAPI's built-in error response when the request has ValidationError's, as described in the FastAPI docs on Handling Errors. I can replicate a similar error format with this example:

from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel, constr

class Credentials(BaseModel):
    password: constr(min_length=8)

app = FastAPI()

@app.post("/login")
async def login(credentials: Credentials):
    print(credentials)  # This is just as an example!
    return JSONResponse(status_code=200)
$ curl -s --header "Content-Type: application/json" --request POST --data '{"password":"xyz"}' http://localhost:8000/login | jq
{
  "detail": [
    {
      "loc": [
        "body",
        "password"
      ],
      "msg": "ensure this value has at least 8 characters",
      "type": "value_error.any_str.min_length",
      "ctx": {
        "limit_value": 8
      }
    }
  ]
}

To change the response body, check the FastAPI docs on Use the RequestValidationError body that shows you can access the default detail list of errors, which you can then edit or copy over to a customized details list, and then set it as the content of the JSON error response.

from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel, constr

class Credentials(BaseModel):
    password: constr(min_length=8)

app = FastAPI()

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    # Get the original 'detail' list of errors
    details = exc.errors()
    modified_details = []
    # Replace 'msg' with 'message' for each error
    for error in details:
        modified_details.append(
            {
                "loc": error["loc"],
                "message": error["msg"],
                "type": error["type"],
            }
        )
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content=jsonable_encoder({"detail": modified_details}),
    )


@app.post("/login")
async def login(credentials: Credentials):
    print(credentials)  # Just as an example!
    return JSONResponse(status_code=200)
$ curl -s --header "Content-Type: application/json" --request POST --data '{"password":"xyz"}' http://localhost:8000/login | jq
{
  "detail": [
    {
      "loc": [
        "body",
        "password"
      ],
      "message": "ensure this value has at least 8 characters",
      "type": "value_error.any_str.min_length"
    }
  ]
}

Of course, you can also simply just define your own custom content and JSONResponse.

Gino Mempin
  • 25,369
  • 29
  • 96
  • 135
  • Is it possible to capture multiple validator error messages in one go when using fastapi and pydantic? Please see my other question: https://stackoverflow.com/questions/71905671/pydantic-cycle-through-all-validators-even-if-one-fails-fastapi/71914006?noredirect=1#comment127111100_71914006 – dataviews Apr 25 '22 at 13:19
  • @dataviews I am AFK now, I'll take a look when I have time, but if I remember correctly, all the validation errors are _already_ returned in the response. It's why `detail` is a list, it should be a list of errors. At least that's what happens on my app, where I use 1 model for the entire request. – Gino Mempin Apr 25 '22 at 13:54
  • 1
    I think that is the case if you don't use a validator(). When you see my other post it may make sense what I am trying to do there. Thanks for your help. – dataviews Apr 25 '22 at 13:56
1

Building upon the previous answer to your question, you could use a validation exception handler, as shown below. Related answers on how to customise FastAPI's or Pydantic's error message can be found here and here, well as here and here.

from fastapi import Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    for error in exc.errors(): 
        error['message'] = error.pop('msg')
    
    return JSONResponse(content=jsonable_encoder({"detail": exc.errors()}), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)

Output:

{
    "detail": [
        {
            "loc": [
                "body",
                "password"
            ],
            "message": "Password should at least 8 characters long.",
            "type": "value_error"
        }
    ]
}
Chris
  • 18,724
  • 6
  • 46
  • 80
1

All the answers that you have been given are correct, but they have only one little tiny problem. In your Api documentation under the /docs or /redoc path, the ValidationError and the HTTPValidationError are still displayed as before.

The ValidationError will return whenever you have conversion error in your api path and fastapi will return a RequestValidationError itself. The one that you handle it by defining a custom exception handler.

The HTTPValidationError will retuen whenever you raise a HTTPException in your api.

Unfortunately, until this version of the framework (v0.92.0), there is still no proper and clean way to solve this problem (according to the discussion that happened here) so to fix that, you have to override two variables of fastapi.openapi.utils module :

validation_error_definition and validation_error_response_definition.

If you check them, they're just a simple and hardcoded dictionary. You can either change these two variables manually, in your main file or you can define a Pydantic model to handle your errors and use It's schema() method to get the openapi schema. I prefer second way

from datetime import datetime

from pydantic import BaseModel
from fastapi.openapi import utils


class CustomRequestValidationError(BaseModel):
    message: str
    type: str
    time: int
    extra: dict


validation_schema = CustomRequestValidationError.schema()

custom_validation_error_definition = dict(validation_schema)

validation_schema.update({"title": "HTTPValidationError"})

custom_validation_error_response_definition = dict(validation_schema)



utils.validation_error_definition = custom_validation_error_definition
utils.validation_error_response_definition = custom_validation_error_response_definition

The point is that in this method we have to use several variables. If the schema() method is used directly, by changing the dictionary, the previous values will also change

0

1- Add Config class to the schema that inheritance from BaseModel.

2- In Config add value_msg_templates dict and start to customize messages: key is error type and value is your new messages for this type.

class UserAuth(BaseModel):
    email: EmailStr = Field(..., description="user email", example="userTest@example.com",)
    username: str = Field(..., min_length=5, max_length=10, description="user username", example="userTest",)
    password: str = Field(..., description="user password", example="Tyu*&^54",)


    class Config:
        error_msg_templates = {
            'value_error.email': 'email address is not valid.',
        }

where is types:

enter image description here

3- add these lines to main.py or app.py file:


from fastapi import FastAPI, Request, status, HTTPException, Response
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from pydantic import ValidationError
from pydantic.error_wrappers import ErrorWrapper 
.
.
.
app = FastAPI()
.
.
.
@app.exception_handler(RequestValidationError)
async def http_exception_accept_handler(request: Request, exc: RequestValidationError) -> Response:
    raw_errors = exc.raw_errors
    error_wrapper: ErrorWrapper = raw_errors[0]
    validation_error: ValidationError = error_wrapper.exc
    overwritten_errors = validation_error.errors()
    return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                        content={"detail": jsonable_encoder(overwritten_errors)},
                        )
.
.
.

Finish :)