bauerji / flask-pydantic

flask extension for integration with the awesome pydantic package
MIT License
352 stars 56 forks source link

Pydantic v2's error object contains a ctx field #86

Open kakakikikeke-fork opened 7 months ago

kakakikikeke-fork commented 7 months ago

If you intentionally raise ValueError, a field called ctx seems to be added. An example of pydantic v2 error object.

{'validation_error': {'body_params': [{'ctx': {'error': ValueError()},                                                                       
                                       'input': 'hawksnowlog3',
                                       'loc': ('name',),                                                                                     
                                       'msg': 'Value error, ',                                                                               
                                       'type': 'value_error',                                                                                
                                       'url': 'https://errors.pydantic.dev/2.5/v/value_error'}]}}

An error object is included in ctx and the error object cannot be serialized to dict, resulting in an error. The traceback is this.

Traceback (most recent call last):
  File "/Users/kakakikikeke/.local/share/virtualenvs/python-try-aR_k1rUJ/lib/python3.11/site-packages/flask/app.py", line 1463, in wsgi_app
    response = self.full_dispatch_request()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/kakakikikeke/.local/share/virtualenvs/python-try-aR_k1rUJ/lib/python3.11/site-packages/flask/app.py", line 872, in full_dispatch_request
    rv = self.handle_user_exception(e)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/kakakikikeke/.local/share/virtualenvs/python-try-aR_k1rUJ/lib/python3.11/site-packages/flask/app.py", line 870, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/kakakikikeke/.local/share/virtualenvs/python-try-aR_k1rUJ/lib/python3.11/site-packages/flask/app.py", line 855, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/kakakikikeke/.local/share/virtualenvs/python-try-aR_k1rUJ/lib/python3.11/site-packages/flask_pydantic/core.py", line 250, in wrapper
    jsonify({"validation_error": err}), status_code
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/kakakikikeke/.local/share/virtualenvs/python-try-aR_k1rUJ/lib/python3.11/site-packages/flask/json/__init__.py", line 170, in jsonify
    return current_app.json.response(*args, **kwargs)  # type: ignore[return-value]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/kakakikikeke/.local/share/virtualenvs/python-try-aR_k1rUJ/lib/python3.11/site-packages/flask/json/provider.py", line 216, in response
    f"{self.dumps(obj, **dump_args)}\n", mimetype=self.mimetype
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/kakakikikeke/.local/share/virtualenvs/python-try-aR_k1rUJ/lib/python3.11/site-packages/flask/json/provider.py", line 181, in dumps
    return json.dumps(obj, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/kakakikikeke/.pyenv/versions/3.11.6/lib/python3.11/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
          ^^^^^^^^^^^
  File "/Users/kakakikikeke/.pyenv/versions/3.11.6/lib/python3.11/json/encoder.py", line 200, in encode
    chunks = self.iterencode(o, _one_shot=True)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/kakakikikeke/.pyenv/versions/3.11.6/lib/python3.11/json/encoder.py", line 258, in iterencode
    return _iterencode(o, 0)
           ^^^^^^^^^^^^^^^^^
  File "/Users/kakakikikeke/.local/share/virtualenvs/python-try-aR_k1rUJ/lib/python3.11/site-packages/flask/json/provider.py", line 121, in _default
    raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")

I solved it by deleting ctx, is there anything else I can do? #84

Thanks.

yctomwang commented 7 months ago

@kakakikikeke-fork Hi there, thank you for the issue and also the PR. I did some investigation today and yesterday, it is indeed a valid issue and would occur on not only things like body_params but also things like query params since they are based on the same fundation. The PR #84 is currently failing the unit test on CI, I belive its caused by calling err.get("body_params") on err that does not have "body_params", so request that only has query_params if that makes sense.

kakakikikeke-fork commented 7 months ago

@yctomwang I tried supporting parameters other than body_params. I have locally run pytest I have also verified that locally all pytest tests are successful. Thanks.

nplebanskyi commented 5 months ago

As a workaround for your custom validations you can use PydanticCustomError from pydantic_core.

from pydantic_core import PydanticCustomError

def custom_validator(value: str) -> str:
    if value == "bad":
        raise PydanticCustomError("bad_str", "bad is not good.")
    return value
nickzorin commented 4 months ago

Instead of removing ctx (or url, or input), it would be better to introduce some arguments to 'validate' decorator, similar to include_context|include_url|include_input and pass it down to ve.errors

yctomwang commented 4 months ago

@nickzorin yep i have to agree with this, removing the ctx would not be the best idea. I am planning to spend some time eventually to deal with this issue, current we are storing the acutal objects hence the reason for the error apprearing. This will require a bit of rework to get all exist test cases to adapt. Also currently the CI is failing because of the pytest black plugin. For some reason I cannot merge to master to address the CI issue.

eytanhanig commented 2 months ago

Any updates on this? I'm planning to introduce flask-pydantic to my org but feel that with all this extra context we are exposing far more info than is desirable.

Alexei-Bogdanov commented 2 months ago

Guys, any updates so far?( this is really really frustrating! This issue literally means that you cannot use all the pydantic field_validator or model_validator by raising python's AssertionError or ValueError. Just a small example:

class RequestBodyModel(BaseModel):
    name: str
    phone_number: Optional[str] = None
    email: Optional[EmailStr] = None

    @model_validator(mode='before')
    @classmethod
    def check_email_or_phone(cls, data: Any) -> Any:
        email = data.get('email')
        phone_number = data.get('phone_number')
        assert bool(email) ^ bool(phone_number), 'Email or phone number must be provided, but not both'

        return data

And obviously because of ctx includes the error as Python object, Flask cannot jsonify the response. @yctomwang I have a simple suggestion as a maybe workaround, just in order to allow Flask does its' JSON serialization job:

  1. According to the Pydantic doc, ctx is a kinda optional, so I do not see any objections removing it from the resulting dict
  2. On the other side, again Pydantic doc has very explicit example of customizing pydantic error messages, leveraging custom convert_errors() func, which can do anything we want, e.g. converting AssertionError/ValueError exception instances to a str using builtin repr():
    >>> repr(err['body_params'][0]['ctx']['error'])
    "AssertionError('Email or phone number must be provided, but not both')"
yctomwang commented 1 month ago

We literally got the ci fixed this week and can finally merge code into the master. After some careful thinking, i think the remove appraoch in #84 might not be the best. We are planning to get this issue addressed as soon as possible. any ideas are welcome