Giter Club home page Giter Club logo

flask-pydantic's Introduction

Flask-Pydantic

Actions Status PyPI Language grade: Python License Code style

Flask extension for integration of the awesome pydantic package with Flask.

Installation

python3 -m pip install Flask-Pydantic

Basics

URL query and body parameters

validate decorator validates query, body and form-data request parameters and makes them accessible two ways:

  1. Using validate arguments, via flask's request variable
parameter type request attribute name
query query_params
body body_params
form form_params
  1. Using the decorated function argument parameters type hints

URL path parameter

If you use annotated path URL path parameters as follows

@app.route("/users/<user_id>", methods=["GET"])
@validate()
def get_user(user_id: str):
    pass

flask_pydantic will parse and validate user_id variable in the same manner as for body and query parameters.


Additional validate arguments

  • Success response status code can be modified via on_success_status parameter of validate decorator.
  • response_many parameter set to True enables serialization of multiple models (route function should therefore return iterable of models).
  • request_body_many parameter set to False analogically enables serialization of multiple models inside of the root level of request body. If the request body doesn't contain an array of objects 400 response is returned,
  • get_json_params - parameters to be passed to flask.Request.get_json function
  • If validation fails, 400 response is returned with failure explanation.

For more details see in-code docstring or example app.

Usage

Example 1: Query parameters only

Simply use validate decorator on route function.

❗ Be aware that @app.route decorator must precede @validate (i. e. @validate must be closer to the function declaration).

from typing import Optional
from flask import Flask, request
from pydantic import BaseModel

from flask_pydantic import validate

app = Flask("flask_pydantic_app")

class QueryModel(BaseModel):
  age: int

class ResponseModel(BaseModel):
  id: int
  age: int
  name: str
  nickname: Optional[str] = None

# Example 1: query parameters only
@app.route("/", methods=["GET"])
@validate()
def get(query: QueryModel):
  age = query.age
  return ResponseModel(
    age=age,
    id=0, name="abc", nickname="123"
    )
See the full example app here
  • age query parameter is a required int
    • curl --location --request GET 'http://127.0.0.1:5000/'
    • if none is provided the response contains:
      {
        "validation_error": {
          "query_params": [
            {
              "loc": ["age"],
              "msg": "field required",
              "type": "value_error.missing"
            }
          ]
        }
      }
    • for incompatible type (e. g. string /?age=not_a_number)
    • curl --location --request GET 'http://127.0.0.1:5000/?age=abc'
      {
        "validation_error": {
          "query_params": [
            {
              "loc": ["age"],
              "msg": "value is not a valid integer",
              "type": "type_error.integer"
            }
          ]
        }
      }
  • likewise for body parameters
  • example call with valid parameters: curl --location --request GET 'http://127.0.0.1:5000/?age=20'

-> {"id": 0, "age": 20, "name": "abc", "nickname": "123"}

Example 2: URL path parameter

@app.route("/character/<character_id>/", methods=["GET"])
@validate()
def get_character(character_id: int):
    characters = [
        ResponseModel(id=1, age=95, name="Geralt", nickname="White Wolf"),
        ResponseModel(id=2, age=45, name="Triss Merigold", nickname="sorceress"),
        ResponseModel(id=3, age=42, name="Julian Alfred Pankratz", nickname="Jaskier"),
        ResponseModel(id=4, age=101, name="Yennefer", nickname="Yenn"),
    ]
    try:
        return characters[character_id]
    except IndexError:
        return {"error": "Not found"}, 400

Example 3: Request body only

class RequestBodyModel(BaseModel):
  name: str
  nickname: Optional[str] = None

# Example2: request body only
@app.route("/", methods=["POST"])
@validate()
def post(body: RequestBodyModel):
  name = body.name
  nickname = body.nickname
  return ResponseModel(
    name=name, nickname=nickname,id=0, age=1000
    )
See the full example app here

Example 4: BOTH query paramaters and request body

# Example 3: both query paramters and request body
@app.route("/both", methods=["POST"])
@validate()
def get_and_post(body: RequestBodyModel,query: QueryModel):
  name = body.name # From request body
  nickname = body.nickname # From request body
  age = query.age # from query parameters
  return ResponseModel(
    age=age, name=name, nickname=nickname,
    id=0
  )
See the full example app here

Example 5: Request form-data only

class RequestFormDataModel(BaseModel):
  name: str
  nickname: Optional[str] = None

# Example2: request body only
@app.route("/", methods=["POST"])
@validate()
def post(form: RequestFormDataModel):
  name = form.name
  nickname = form.nickname
  return ResponseModel(
    name=name, nickname=nickname,id=0, age=1000
    )
See the full example app here

Modify response status code

The default success status code is 200. It can be modified in two ways

  • in return statement
# necessary imports, app and models definition
...

@app.route("/", methods=["POST"])
@validate(body=BodyModel, query=QueryModel)
def post():
    return ResponseModel(
            id=id_,
            age=request.query_params.age,
            name=request.body_params.name,
            nickname=request.body_params.nickname,
        ), 201
  • in validate decorator
@app.route("/", methods=["POST"])
@validate(body=BodyModel, query=QueryModel, on_success_status=201)
def post():
    ...

Status code in case of validation error can be modified using FLASK_PYDANTIC_VALIDATION_ERROR_STATUS_CODE flask configuration variable.

Using the decorated function kwargs

Instead of passing body and query to validate, it is possible to directly defined them by using type hinting in the decorated function.

# necessary imports, app and models definition
...

@app.route("/", methods=["POST"])
@validate()
def post(body: BodyModel, query: QueryModel):
    return ResponseModel(
            id=id_,
            age=query.age,
            name=body.name,
            nickname=body.nickname,
        )

This way, the parsed data will be directly available in body and query. Furthermore, your IDE will be able to correctly type them.

Model aliases

Pydantic's alias feature is natively supported for query and body models. To use aliases in response modify response model

def modify_key(text: str) -> str:
    # do whatever you want with model keys
    return text


class MyModel(BaseModel):
    ...
    model_config = ConfigDict(
        alias_generator=modify_key,
        populate_by_name=True
    )

and set response_by_alias=True in validate decorator

@app.route(...)
@validate(response_by_alias=True)
def my_route():
    ...
    return MyModel(...)

Example app

For more complete examples see example application.

Configuration

The behaviour can be configured using flask's application config FLASK_PYDANTIC_VALIDATION_ERROR_STATUS_CODE - response status code after validation error (defaults to 400)

Additionally, you can set FLASK_PYDANTIC_VALIDATION_ERROR_RAISE to True to cause flask_pydantic.ValidationError to be raised with either body_params, form_params, path_params, or query_params set as a list of error dictionaries. You can use flask.Flask.register_error_handler to catch that exception and fully customize the output response for a validation error.

Contributing

Feature requests and pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

  • clone repository
    git clone https://github.com/bauerji/flask_pydantic.git
    cd flask_pydantic
  • create virtual environment and activate it
    python3 -m venv venv
    source venv/bin/activate
  • install development requirements
    python3 -m pip install -r requirements/test.pip
  • checkout new branch and make your desired changes (don't forget to update tests)
    git checkout -b <your_branch_name>
  • run tests
    python3 -m pytest
  • if tests fails on Black tests, make sure You have your code compliant with style of Black formatter
  • push your changes and create a pull request to master branch

TODOs:

  • header request parameters
  • cookie request parameters

flask-pydantic's People

Contributors

adriencaccia avatar bauerji avatar buildpeak avatar cardoe avatar chr0m1ng avatar jcreekmore avatar jkseppan avatar kilo59 avatar laanak08 avatar marimelon avatar omarthinks avatar shadchin avatar vishket avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

flask-pydantic's Issues

add py.typed marker indicating provision of inlined types

when enabling pylance/pyright typing i encountered some errors related to flask-pydantic. when i reached out to the maintainer

microsoft/pyright#4733 (comment)

he provided this feedback.

The flask_pydantic package appears to have inline type annotations within its own code. However, it is not marked as "py.typed" (i.e. it doesn't have a "py.typed" marker file indicating that it has inlined types). You may want to reach out to the maintainers of the library to encourage them to add this marker file. For more details, refer to this documentation.

i'm happy to give this a shot with a PR but wanted to have some discussion about it first. Thanks in advance!

Validate request.form arguments from HTML post form

Hi,

not sure whether there is an option to validate request.form arguments right now, with this package but it would be a nice-to-have feature :)

For example, to use like this:
@validate(form=SomeModel)

Right now, I am validating request.form simply like this:

    try:
        form = LoginFormModel(**request.form)
    except ValidationError as ve:
        return {"validation_error": ve.errors()}, 400

Or is there any other way around?

Any advice would be much appreciated!

annotations bug

When
from __future__ import annotations
is imported got following error:

        **{
            key: value
            for key, value in query_params.to_dict(flat=False).items()
>           if key in model.__fields__ and model.__fields__[key].is_complex()
        },
    }
E   AttributeError: 'str' object has no attribute '__fields__'

local model variable became str instead of pydantic class

Virtualenv
Python: 3.11.7
Implementation: CPython

System
Platform: darwin
OS: posix
Python: 3.11.7

Support union types in request body

Is there a way to have a request which can dynamically accept two (or more) different models?

e.g.

@validate(body=Union[ModelA, ModelB])
def post():

Is it possible for the deserialisation to then be dynamic and the function can check request.body_params using isinstance?

Aliased list query parameters do not work

flask-pydantic==0.10.0
flask==2.1.2

Steps to reproduce

  1. Define a query model with an aliased list field.
class QueryModel(BaseModel):
    order_by: Optional[
        List[Literal["created", "name"]]
    ] = Field(..., alias="order-by")

    class Config:
        allow_population_by_field_name = True
  1. Set this as query: QueryModel in your request handler function signature.
  2. Send a request with ?order-by=name

Expected result

  • The request should be considered valid.
  • query.order_by should be a list ['name'].

Actual result

{
  "validation_error": {
    "query_params": [
      {
        "loc": [
          "order-by"
        ], 
        "msg": "value is not a valid list", 
        "type": "type_error.list"
      }
    ]
  }
}

In short:

  • Aliased query params work.
  • List query params work (e.g. ?order_by=name&order_by=created gives you list with these values).
  • Aliased list query params do not work.

Does anyone use with class based views?

Generally it works fine to add "validate" to the decorators attribute of the class, however when you want to return a list what is the best practice to include response_many=True in the kwargs?

One option is to roll our own decorator using something like functools.partial. Any otehr ideas?

Error if validation error contains enum.Enum

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.

Flask Get Request validate failed

from pydantic import BaseModel
from typing import List
class Query(BaseModel):
    query: str
    
@test_bp.route("/test_route")
@validate(query=Query)
def test_route(query:Query):
    return {}

my code is very simple, but when i send 'http://127.0.0.1:8800/test_route?query=1',I will receive

{
  "validation_error": {
    "query_params": [
      {
        "loc": [
          "query"
        ], 
        "msg": "str type expected", 
        "type": "type_error.str"
      }
    ], 
  }
}

I try add more detail in core.py

try:
                    q = query_model(**query_params)
                except ValidationError as ve:
                    err["query_params"] = ve.errors()
                    err["value"] = query_params

I will get

"value": {
      "query": [
        "1"
      ]
    }

So I'm very doubt why query becomes list rather than str.

My python requrement is

python3.6
pydantic==1.7.3
flask-pydantic-spec==0.1.3
Flask==1.1.2
dataclasses==0.8

Looking forward to a reply!

Request change: the example in the main README.md file is not clear

Before I start, I really thank you so much for this package.


I request to make a change:
The example at the README.md file in the main page of the package is not clear.
At first, I thought that the package was not working correctly.
I kept working, and sending requests till finally worked after about 15 minutes of trying.


For this reason:
The example is very advanced, and for someone who wanna learn the package for the first time, the learning curve of the example is very hard to learn.
This is dangerous for someone who wants to learn about the package for the first time.


So I would like to solve this problem by doing this.
Create 3 separate endpoints:

  1. To read the GET request only, and above it the pydantic class of it.
  2. To read the JSON request body only, and above it the pydantic class of it.
  3. To read both the JSON request and the GET request together, using the old pydantic classes.

If God willed, I can make this change.
I will make these changes, If you give the permission.
Thank you!

AttributeError: 'FieldInfo' object has no attribute 'is_complex'

Hii All,

Recently, In my project to build a recommendation system with flask_restful, I extensively used flask_pydantic but faced the error that is shown in title.

This error is raised when I tried to use query like shown here (line 61, it is the same code as in documentation)

I extensively created a github repo to diagnose this issue and it can be examined here

The issue exists with vanilla as well as RESTful flask api.

Flask-Pydantic==0.11.0
Flask-RESTful==0.3.10
pydantic==2.4.2

Pinned version of Pydantic

Hi,

You appear to have pinned version 1.7.1 of Pydantic in base.pip. Would it be possible to remove this pinned version, or to change it to a minimum version? Thanks!

Untyped decorator makes function "index" untyped

I am using Flask-Pydantic with mypy and I got the error

error: Untyped decorator makes function "index" untyped

on the line @validate()

@blueprint.post("/")
@validate()
def index(body: RequestBodyModel) -> UserResponse:

Support of path parameters

So far the lib handles:

  • query parameters /endpoint?q=foo
  • post parameters /endpoint -d '{"q":"foo"}'

Could it handle pathParam /endpoint/{q} ?

[Feature request] Pass body and query as kwargs

Hello, I would like to discuss the following feature:

Having the ability to have the potential query and body objects directly passed as kwargs to the method.

The two reasons I like this feature:

  1. The code is easier to understand, you do not have to go through the request object.
  2. If you add the type hints to the arguments (as shown in the example below), query and body will be correctly typed in your IDE. This brings a much better developer experience.

In the example below, the validate_and_parse is doing the same thing as validate would except it populates the decorated function kwargs.

...
from flask_pydantic import validate_and_parse

...

class QueryModel(BaseModel):
    age: int


class BodyModel(BaseModel):
    name: str
    nickname: Optional[str]


class ResponseModel(BaseModel):
    id: int
    age: int
    name: str
    nickname: Optional[str]


@app.route("/", methods=["POST"])
@validate_and_parse(body=BodyModel, query=QueryModel)
def post(query: QueryModel, body: BodyModel):
    """
    Basic example with both query and body parameters, response object serialization.
    """
    # save model to DB
    id_ = 2

    return ResponseModel(
        id=id_,
        age=query.age,
        name=body.name,
        nickname=body.nickname,
    )

If you are interested in this feature, I would really love to have a crack at it.
I already have a quick implementation ready.

When I use GET and the Content-Type in the header is application / json, body_params = request.get_json () in the core file will report werkzeug.exceptions.BadRequest

When I use GET and the Content-Type in the header is application / json, body_params = request.get_json () in the core file will report werkzeug.exceptions.BadRequest

[2020-01-15 14:00:21] [ERROR] [logs] [__call__] [52] Traceback (most recent call last):
  File "D:\Development_language\pyenv\api\lib\site-packages\werkzeug\wrappers\json.py", line 119, in get_json
    rv = self.json_module.loads(data)
  File "D:\Development_language\pyenv\api\lib\site-packages\flask\json\__init__.py", line 253, in loads
    return _json.loads(s, **kwargs)
  File "d:\development_language\pyhton37\Lib\json\__init__.py", line 361, in loads
    return cls(**kw).decode(s)
  File "d:\development_language\pyhton37\Lib\json\decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "d:\development_language\pyhton37\Lib\json\decoder.py", line 355, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "D:\isizy\services\api\app\models\logs.py", line 49, in __call__
    response = self._func(*args, **kwargs)  # 执行api
  File "D:\isizy\services\api\api_lib\flask_pydantic\core.py", line 116, in wrapper
    body_params = request.get_json()
  File "D:\Development_language\pyenv\api\lib\site-packages\werkzeug\wrappers\json.py", line 128, in get_json
    rv = self.on_json_loading_failed(e)
  File "D:\Development_language\pyenv\api\lib\site-packages\flask\wrappers.py", line 27, in on_json_loading_failed
    raise BadRequest()
werkzeug.exceptions.BadRequest: 400 Bad Request: The browser (or proxy) sent a request that this server could not understand.

[2020-01-15 14:00:21] [ERROR] [wsgi] [_log] [196] 500 GET /api/scores/da1c17ae-1d4e-11ea-be9b-0242ac130005/trend/data (192.168.1.45) 381.94ms

This is my request:
image

Change the project name to "flask-pydantic"

As indicated in the setup.py, this project's name is "Flask-Pydantic", so the name in the GitHub URL should be "flask-pydantic" instead of "flask_pydantic". I would recommend changing the name to "flask-pydantic" to make it consistent with PyPI page.

Since GitHub will redirect the old URL to the new one, it should be easy to change it before the project becomes more popular.

Validation for Array of Models fails

from typing import List
from flask_pydantic import validate

api = Blueprint("lineage-collector", __name__)

class EntityPayload(BaseModel):
    attributes: dict
    typeName: str

class EntityPayloadBulk(BaseModel):
    __root__:  List[EntityPayload]

@api.route('/entity/bulk', methods=["POST"])
@validate()
def entity_bulk(body: EntityPayloadBulk):
    return "Hello World"

Call to this endpoint returns the following error:

` File "/usr/local/lib/python3.9/site-packages/flask_pydantic/core.py", line 158, in wrapper
b = body_model(**body_params)
TypeError: api.routes.EntityPayloadBulk() argument after ** must be a mapping, not list

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 2464, in call
return self.wsgi_app(environ, start_response)
File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 2450, in wsgi_app
response = self.handle_exception(e)
File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 1867, in handle_exception
reraise(exc_type, exc_value, tb)
File "/usr/local/lib/python3.9/site-packages/flask/_compat.py", line 39, in reraise
raise value
File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 2447, in wsgi_app
response = self.full_dispatch_request()
File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 1952, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 1821, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/usr/local/lib/python3.9/site-packages/flask/_compat.py", line 39, in reraise
raise value
File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 1950, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 1936, in dispatch_request
return self.view_functionsrule.endpoint
File "/usr/local/lib/python3.9/site-packages/flask_pydantic/core.py", line 164, in wrapper
raise JsonBodyParsingError()
flask_pydantic.exceptions.JsonBodyParsingError`

Dealing with Query Param Arrays

Another question - how would I go about validating query param arrays?

For example I have code like this:

response = test_client.get("/api/v1/services", query_string={"ids[]": ["5f03539f9472e7f1a153797d"]})

assert response.status_code == 200
@services_bp.route("", methods=["GET"], strict_slashes=False)
@validate(query=GetObjectIds)
def get_services():
    """
    Retrieves list of all services
    """
    data = request.args.getlist("ids[]")

    if not data:
        services = service_db.all_services()
    else:
        services = service_db.get_services([x for x in data])

    if len(services) > 0:
        return jsonify([service.dict() for service in services])
    else:
        return jsonify(error="No services found with requested ids"), 404

I'd like to be able to have a model that says "validate that the get query correctly contains an id of lists."

how to use flask getlist mothed

if my query string is

http://url?a[]=1&a[]=2&a[]=3

i want the get the a value is a = [1, 2, 3]

can i use the query: useModel realize it ?

class useModel(BaseModel):
    a: List = Field([]) 

is not working

Is there any way to make custom response?

Hello! Thanks for awesome package.

I want to make custom response

ASIS

{
  "validation_error": {
    "query_params": [
      {
        "loc": ["age"],
        "msg": "value is not a valid integer",
        "type": "type_error.integer"
      }
    ]
  }
}

TOBE(or some other fomat, maybe)

{"error" : "validation_error", 
"desc" : "some_my_custome_message"}

Thanks for reading this.

pydantic tip...

Would anyone be able to give me a Pydantic tip at all?

Is it possible on a pydantic model to reference another class? For example below in the ReadRequestModel in point_type I am trying to figure out if its possible reference that only these "types of points" in a string format can be chosen:

# type-of-points
# just for reference
multiStateValue
multiStateInput
multiStateOutput
analogValue
analogInput
analogOutput
binaryValue
binaryInput
binaryOutput

And depending on what point_type is that depics what type of point_id can be chosen that I am trying to reference in the PointType class twice below that is obviously very wrong. Any tips greatly appreciated...

from typing import List, Literal, Optional
from pydantic import BaseModel


BOOLEAN_ACTION_MAPPING = Literal["active", "inactive"]


class ReadRequestModel(BaseModel):
    device_address: str
    point_type: PointType  <--- not correct
    point_id: PointType    <--- not correct


class PointType(BaseModel):
    multiStateValue: Optional[int]
    multiStateInput: Optional[int]
    multiStateOutput: Optional[int]
    analogValue: Optional[int]
    analogInput: Optional[int]
    analogOutput: Optional[int]
    binaryValue: Optional[BOOLEAN_ACTION_MAPPING]
    binaryInput: Optional[BOOLEAN_ACTION_MAPPING]
    binaryOutput: Optional[BOOLEAN_ACTION_MAPPING]


r = ReadRequestModel({'device_address': '12345:5',
                     'point_type': 'analogInput',
                      'point_id': 8})

print(r)

Validation of ResponseModel?

Is it possible to validate that ResponseModel is what's returned? Hoping I've missed something obvious in the docs: I see query, body, and form, along with the ability to generate response_many.

Extending the sample app.py a tiny bit:

from typing import Optional
from flask import Flask, request
from pydantic import BaseModel

from flask_pydantic import validate

app = Flask("flask_pydantic_app")

class QueryModel(BaseModel):
  age: int

class ResponseModel(BaseModel):
  id: int
  age: int
  name: str
  nickname: Optional[str] = None

# Example 1: query parameters only
@app.route("/", methods=["GET"])
@validate()
def get(query: QueryModel) -> ResponseModel:
  age = query.age
  if age > 10:
      return {'asdf': False}. # This should NOT be a valid response
  return ResponseModel(
    age=age,
    id=0, name="abc", nickname="123"
    )

Now querying and getting a response that doesn't match the expected ResponseModel:

❯ curl 'http://localhost:5000/?age=1'
{"id":0,"age":1,"name":"abc","nickname":"123"}
❯ curl 'http://localhost:5000/?age=100'
{"asdf":false}

Was hoping that there was a way to ensure that an invalid response schema couldn't be returned. Any pointers?

Raise classical Pydantic ValidationError like FastApi

Hello,

I'm working with this library and I found the option to raise errors (FLASK_PYDANTIC_VALIDATION_ERROR_RAISE = True).

I was expecting the same kind of error as in FastAPI/Pydantic combination:

{
   "errors":[
      {
         "loc":[
            "query",
            "request-mode"
         ],
         "msg":"field required",
         "type":"value_error.missing"
      },
      {
         "loc":[
            "body",
            "birth_date"
         ],
         "msg":"field required",
         "type":"value_error.missing"
      }
   ]
}

In Pydantic, all errors are in the errors array and the location (header, body...) is specified directly in "loc".

In Flask-Pydantic, errors are in separate folders according to the location:

{
   "body":[
      {
         "loc":[
            "birth_date"
         ],
         "msg":"field required",
         "type":"value_error.missing"
      }
   ],
   "query":[
      {
         "loc":[
            "request-mode"
         ],
         "msg":"field required",
         "type":"value_error.missing"
      }
   ]
}

The ValidationError(BaseFlaskPydanticException) exception e is raised and you can look for each group errors according to the location:

  • e.body_params
  • e.form_params
  • e.path_params
  • e.query_params

What I would like is, for instance, to add the e.errors category which contains all the errors, formatted as in the Pydantic library used by FastAPI.

Thank you!

Request change: passing the validated results to the route as parameters

Request change: passing the validated results to the route as parameters


I can make this change.
After the data have been validated by the decorator,
Now the developer has to validate them again inside the route.


What if I could pass them after they have clean and validated into the route.
They will be passed from the decorator to the route automatically.
And the user will not have to validate them again in the route.

So the code will look like this:

@app.route("/", methods=["POST"])
@validate(body=BodyModel, query=QueryModel)
def post(requested):
	#Requested is validated
	body = requested.body
	# This is body after validation
	query = requested.query
	# This is query after validation
	name = body["name"]
	nickname = body["nickname"]
	age = query["age"]

This is all what the developer will have to write.
Cleaner code.


If God willed, I can make this change.
I request your permission to make this change
Thank you!

Using flask-pydantic with Python Dependency Injection Framework

Hi! I am using this module with Python Dependency Injection Framework and when i try to use Provider in kwargs of the view i am getting:
RuntimeError: no validator found for <class '...'>, see arbitrary_types_allowed in Config
Reason of this error is the fact that @Validate decorator ignores only kwargs named as "query", "body", "return".
It would be better if @Validate decorator ignored kwargs which default is Provide instance

Sample code:

@bp.route('/api/posts', methods=['GET'])
@inject
@validate(response_many=True)
def get_posts(
        service: PostService = Provide[Container.services.post_service]
) -> List[ReadPost]:
    ...

Incompatible with pydantic 2.0.1 - Error validation with GET

class SampleModel(BaseModel):
    modelcode: str

@personalizations_blueprint.route('/data', methods=['GET'])
@jwt_required()
@validate()
def getData(query: SampleModel):
   .....


Validating against this model produces the following error:

AttributeError: 'FieldInfo' object has no attribute 'is_complex'

I'm using pydantic 2.0.1 with Flask-Pydantic 0.11.0

After a bit checking, I've found that downgrading pydantic to version 1 works. So I think you need to update your support for pydantic version 2.

Validator with query params

Hi!
When I'm writing a validator in a pydantic schema, beside getting the values, I need to have the query params in order to know the context of the request.
After seeing the b = body_model(**body_params) in this repo's code I understand that it's not possible at the moment.

I'm thinking about adding it to this package, does that make sense?

Overriding http return stats on exception

I notice that you can update the returned status on success, but can you do the same thing on error? We typically use 422s instead of 400s for malformed requests.

Thanks!

Support of module pydantic.v1 Since update to Pydantic V2

Hello,

Since the V2 update, Pydantic V2 still includes the V1 models to make the migration easier : https://docs.pydantic.dev/latest/migration/#continue-using-pydantic-v1-features

Example :

 from pydantic.v1 import BaseModel

Unfortunately, it seems flask-pydantic dropped the V1 support when it started to support Pydantic V2 (from version 0.12.0).
So, if we have to upgrade, we cannot use the pydantic.v1 legacy models with Flask-Pydantic and we have to migrate the whole project's models to Pydantic V2 models directly. Right?

when response_many is set, can't handle returning errors

When a route that has repononse_many wants to return a proper error code on a, flask-pydantic crashes with

This should return a 400 error

@app.route("/<myid>", methods=["GET"])
@pydantic_validate(response_many=True)
def return_many(myid: str):
    if not_in_db(myid):
        return jsonify({"success": False, "message": f"{myid} not found in DB"}), 404
....

But instead, the server breaks and raises a exception:

flask_pydantic.exceptions.InvalidIterableOfModelsException: (<Response 86 bytes [200 OK]>, 404)

There should be some way to describe the error responses that shouldn't be a an array.

Pydantic v2's error object contains a ctx field

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.

Model.json() vs jsonify

Hello,

Thanks for the app! We were beginning to play around with it and wondering if the returned json could be wrapped in jsonify or if there is an advantage to not to. We have our jsonify set just right to play nice with Mongo, etc., so we wanted to make sure we didn't lose any of its advantages.

Thanks!

Internal Server Error when trying to deserialise body when Content-Type is not application/json

I get the following error when trying to hit "/register" with incorrect Content-Type as text/plain

[2020-06-11 10:15:12,484] ERROR in app: Exception on /register [POST]
Traceback (most recent call last):
File "/home/aguru/.local/lib/python3.7/site-packages/flask/app.py", line 2447, in wsgi_app
response = self.full_dispatch_request()
File "/home/aguru/.local/lib/python3.7/site-packages/flask/app.py", line 1952, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/home/aguru/.local/lib/python3.7/site-packages/flask/app.py", line 1821, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/home/aguru/.local/lib/python3.7/site-packages/flask/_compat.py", line 39, in reraise
raise value
File "/home/aguru/.local/lib/python3.7/site-packages/flask/app.py", line 1950, in full_dispatch_request
rv = self.dispatch_request()
File "/home/aguru/.local/lib/python3.7/site-packages/flask/app.py", line 1936, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/usr/local/lib/python3.7/dist-packages/flask_pydantic/core.py", line 131, in wrapper
b = body(**body_params)
TypeError: ModelMetaclass object argument after ** must be a mapping, not NoneType
127.0.0.1 - - [11/Jun/2020 10:15:12] "POST /register HTTP/1.1" 500 -

Postman
Screenshot from 2020-06-11 10-26-32

Code

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

app = Flask(__name__)


class BodyModel(BaseModel):
    service: str
    details: Optional[str]


class ResponseModel(BaseModel):
    uuid: str
    status: Optional[str]


@app.route("/register", methods=["POST"])
@validate(body=BodyModel)
def register_service_request():
    # save model to DB
    uuid = "uuid"

    return ResponseModel(uuid=uuid), 202


app.run()

I would have expected either:

  1. Should it be Bad Request (or may be Validation Error) instead? We are expecting a json here but got text?
  2. Try to get_json (force=True) even when Content-Type is not application/json. (Does not seem to be proper way of doing it though)

@bauerji Your thoughts?

Question: partially populated nested object with required fields

Hey all,

I am trying to understand if the behavior I am seeing is expected, a bug or misconfiguration of my setup.

The following code uses. Flask-Pydantic = "~=0.9.0"

There's an endpoint that expects the following simple payload. The address is expected to be fully populated (all fields are required).

class Address(BaseModel):
    street: str
    city: str
    region: str
    zipcode: str

class Payload(BaseModel):
    username: str
    address: Optional[Address] = None

    @root_validator
    def validate_address(cls, values):
        # address is missing from values for partially populated address fields
        if "address" not in values or not values["address"]:
            raise ValueError(f"Full address is required")
        return values

This works nicely for fully populated address and when the address is missing in the request (set to None by default).

However, when I send a partially-populated Address, the field address is missing from the payload :

Request payload :

{
"username": "foo",
"address": {
            "zipcode": "10001",
        },
    }

flask body:

{ 'username': 'foo' }

Is that expected that when a pydantic object fails to create, it will be dropped ?

How can I give custom params for Request.get_json(force=False, silent=False, cache=True) ?

I saw that get_json() method have no any params in line:
https://github.com/bauerji/flask_pydantic/blob/45987a2d4474b5c9423e057a66d98863468fa0bd/flask_pydantic/core.py#L176

but the flask support some custom params, in doc:
https://tedboy.github.io/flask/generated/generated/flask.Request.get_json.html

Currently, there was some errors when a post request have no request body:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n<title>400 Bad Request</title>\n<h1>Bad Request</h1>\n<p>Failed to decode JSON object: Expecting value: line 1 column 1 (char 0)</p><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n<title>400 Bad Request</title>\n<h1>Bad Request</h1>\n<p>Failed to decode JSON object: Expecting value: line 1 column 1 (char 0)</p>

Can this library integrate openapi

In this way

@app.route("/kwargs")
@validate()
def test_route_kwargs(query:Query, body:Body)->MyModel:
    return MyModel(...)

In : test_route_kwargs.__annotations__
Out:
{'query': Query,
' body':Body,
 'return': MyModel}

It can get all the conditions for generating openapi.

Is there a way to convert key casing in responses?

I'd like to have my Pydantic models defined in snake_case but have the API return JSON with camelCase fields.

In FastAPI, I can do the following:

def to_camel(string: str):
    # returns "string" converted to camelCase

class MyModel(BaseModel):
    foo_bar: str

    class Config:
        alias_generator = to_camel
        allow_population_by_field_name = True

And the resulting JSON will be {"fooBar": "x"}. This is similar to the way it's described in the Pydantic docs.

This doesn't work with flask-pydantic. Is there any way to achieve similar behaviour?

Pyright error report

if the validate decorator decorates a function that has multiple arguments, then PyRight will report a reportGeneralTypeIssues like the picture attached:

image

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.