bauerji / flask-pydantic

flask extension for integration with the awesome pydantic package
MIT License
358 stars 57 forks source link

Error if validation error contains enum.Enum #54

Open bruno-robert opened 2 years ago

bruno-robert commented 2 years ago

This error is occurring for me when the body of an endpoint receives a pydantic.BaseModel that contains a field of type Enum and the validation fail, the error message returned is a JSON serialization error instead of a ValidationError.

Example validation that fail correctly:

from flask import Flask
from flask_pydantic import validate
from pydantic import BaseModel, validator

app = Flask(__name__)

class RequestBody(BaseModel):
    format: str

@app.route("/", methods=["POST"])
@validate()
def export(body: RequestBody):
    print(body.format)
    return f"{body.format}"

if __name__ == '__main__':
    app.config["TESTING"] = True
    client = app.test_client()

    valid_data = {"format": "csv"}
    invalid_data = {"format": [123,123]}

    valid_response = client.post("/", json=valid_data)
    print(valid_response.json)

    invalid_response = client.post("/", json=invalid_data)
    print(invalid_response.json)

response:

csv
None
{'validation_error': {'body_params': [{'loc': ['format'], 'msg': 'str type expected', 'type': 'type_error.str'}]}}

the response details why the validation fails

Example validation that fails with a JSON serialization error because of an Enum:

from enum import Enum

from flask import Flask
from flask_pydantic import validate
from pydantic import BaseModel

app = Flask(__name__)

class Formats(Enum):
    CSV = "csv"
    HTML = "html"

class RequestBody(BaseModel):
    format: Formats = Formats.CSV

@app.route("/", methods=["POST"])
@validate()
def export(body: RequestBody):
    return f"{body.format}"

if __name__ == '__main__':
    app.config["TESTING"] = True
    client = app.test_client()

    valid_data = {"format": "csv"}
    invalid_data = {"format": "notcsv"}

    valid_response = client.post("/", json=valid_data)
    print(valid_response.json)
    invalid_response = client.post("/", json=invalid_data)
    print(invalid_response.json)

response (with stack trace):

Traceback (most recent call last):
  File "/Users/bruno/Developer/flask_pydantic_error/main.py", line 34, in <module>
    invalid_response = client.post("/", json=invalid_data)
  File "/Users/bruno/Developer/flask_pydantic_error/venv/lib/python3.10/site-packages/werkzeug/test.py", line 1140, in post
    return self.open(*args, **kw)
  File "/Users/bruno/Developer/flask_pydantic_error/venv/lib/python3.10/site-packages/flask/testing.py", line 217, in open
    return super().open(
  File "/Users/bruno/Developer/flask_pydantic_error/venv/lib/python3.10/site-packages/werkzeug/test.py", line 1089, in open
    response = self.run_wsgi_app(request.environ, buffered=buffered)
  File "/Users/bruno/Developer/flask_pydantic_error/venv/lib/python3.10/site-packages/werkzeug/test.py", line 956, in run_wsgi_app
    rv = run_wsgi_app(self.application, environ, buffered=buffered)
  File "/Users/bruno/Developer/flask_pydantic_error/venv/lib/python3.10/site-packages/werkzeug/test.py", line 1237, in run_wsgi_app
    app_rv = app(environ, start_response)
  File "/Users/bruno/Developer/flask_pydantic_error/venv/lib/python3.10/site-packages/flask/app.py", line 2091, in __call__
    return self.wsgi_app(environ, start_response)
  File "/Users/bruno/Developer/flask_pydantic_error/venv/lib/python3.10/site-packages/flask/app.py", line 2076, in wsgi_app
    response = self.handle_exception(e)
  File "/Users/bruno/Developer/flask_pydantic_error/venv/lib/python3.10/site-packages/flask/app.py", line 2073, in wsgi_app
    response = self.full_dispatch_request()
  File "/Users/bruno/Developer/flask_pydantic_error/venv/lib/python3.10/site-packages/flask/app.py", line 1519, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/Users/bruno/Developer/flask_pydantic_error/venv/lib/python3.10/site-packages/flask/app.py", line 1517, in full_dispatch_request
    rv = self.dispatch_request()
  File "/Users/bruno/Developer/flask_pydantic_error/venv/lib/python3.10/site-packages/flask/app.py", line 1503, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
  File "/Users/bruno/Developer/flask_pydantic_error/venv/lib/python3.10/site-packages/flask_pydantic/core.py", line 212, in wrapper
    return make_response(jsonify({"validation_error": err}), status_code)
  File "/Users/bruno/Developer/flask_pydantic_error/venv/lib/python3.10/site-packages/flask/json/__init__.py", line 302, in jsonify
    f"{dumps(data, indent=indent, separators=separators)}\n",
  File "/Users/bruno/Developer/flask_pydantic_error/venv/lib/python3.10/site-packages/flask/json/__init__.py", line 132, in dumps
    return _json.dumps(obj, **kwargs)
  File "/Users/bruno/.pyenv/versions/3.10.3/lib/python3.10/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "/Users/bruno/.pyenv/versions/3.10.3/lib/python3.10/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/Users/bruno/.pyenv/versions/3.10.3/lib/python3.10/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "/Users/bruno/Developer/flask_pydantic_error/venv/lib/python3.10/site-packages/flask/json/__init__.py", line 51, in default
    return super().default(o)
  File "/Users/bruno/.pyenv/versions/3.10.3/lib/python3.10/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type Formats is not JSON serializable
None

When I was expecting an response of type:

csv
None
{'validation_error': {'body_params': [{'ctx': {'enum_values': ['csv', 'html']}, 'loc': ['format'], 'msg': "value is not a valid enumeration member; permitted: 'csv', 'html'", 'type': 'type_error.enum'}]}}

It seems like the flask's JSONEncoder is used instead of std json. If I modify flask.json.JSONEncoder's default method in order to add:

if isinstance(o, Enum):
    return o.value

the program functions as expected.

cardoe commented 2 years ago

Have you tried class Formats(str, Enum):

bruno-robert commented 1 year ago

Sorry for the delayed reply. First off, thank you for the response! I have been very busy lately and haven't had time to try this. I'll update this thread when I do ;)

aberaut commented 1 year ago

I'm running into the same issue. @cardoe is the ask here to have all enum classes extend str? This seems like it would have adverse effects on type safety

cardoe commented 1 year ago

I'm running into the same issue. @cardoe is the ask here to have all enum classes extend str? This seems like it would have adverse effects on type safety

No. You need to extend it with the actual type you are serializing your enum data out with. JSON cannot encode enum's natively so you use another type. So in OPs example they are using str values. So they need to do that. If you use numbers then you should use int. Does that make sense?