flask-openapi3 icon indicating copy to clipboard operation
flask-openapi3 copied to clipboard

Response Data Validation in flask-openapi3

Open wconrad265 opened this issue 11 months ago • 3 comments

Summary

Currently, our usage of flask-openapi3 relies on the responses defined in the route definition to generate schemas. However, for my current project, I need to implement validation for the data leaving the Flask server as well.

I have already written a basic version of this code, which is easy to implement, and I would like to propose adding this functionality as a feature to the library.

Feature Overview:

Response Data Validation: The goal is to validate the response data before it is sent to the client. If the data is invalid (e.g., a missing field), it should result in a server error, indicating that the application code is broken.

Integration with OpenAPI: We will use the defined JSON Schema for the response in the OpenAPI path operation for validation. This schema will not only be used to generate automatic documentation but also be leveraged by client code generation tools, ensuring consistency across the stack.

Feature Control: I have already implemented a basic version of this functionality, which is controlled via a boolean flag that triggers response validation.

For example, in the code snippet below, since validate_response is set to true, the response returned by return {"code": 0, "message": "ok", "data": {}}, will validated against the BookResponse pydantic model.

By default, validate_response would have a default value of false. This would be an optional feature that people could opt into.

class BookBodyWithID(BaseModel):
    bid: int = Field(..., description='book id')
    age: Optional[int] = Field(None, ge=2, le=4, description='Age')
    author: str = Field(None, min_length=2, max_length=4, description='Author')


class BookResponse(BaseModel):
    code: int = Field(0, description="status code")
    message: str = Field("ok", description="exception information")
    data: BookBodyWithID


@app.get('/book/<int:bid>', 
         tags=[book_tag], 
         responses={
             200: BookResponse, 
             # Version 2.4.0 starts supporting response for dictionary types
             201: {"content": {"text/csv": {"schema": {"type": "string"}}}}
         }, validate_response=true)
def get_book(path: BookPath, query: BookBody):
    """get a book
    get book by id, age or author
    """
    return {"code": 0, "message": "ok", "data": {}}

Furthermore, this also helps ensure that the docs created will always match the response, since the response will be validated against the schema.

If there is interested in adding this, let me know I can create a pr in my free time, and update the documentation.

wconrad265 avatar Mar 11 '25 17:03 wconrad265

In fact, this feature was turned off(#39) due to its complexity.

Since many people later needed this feature, I think it can be redesigned.

I have some suggestions:

  1. In OpenAPI (app instance), adding validate_response can propagated to all APIBlueprint and APIs decorated by the app.
  2. In APIBlueprint, adding validate_response can be propagated to the APIs decorated with APIBlueprint.
  3. Add validate_response in the API, applying it to a single API.

@wconrad265 welcome to submit PR.

@ddorian @mr-tabasco Hope we can work together to achieve this feature.

luolingchun avatar Mar 12 '25 01:03 luolingchun

I would like it to do validation and dumping like: BookResponse.model_validate(data).model_dump(). Maybe that part can be overridden so we can have both ways.

ddorian avatar Mar 12 '25 10:03 ddorian

Thanks everyone. I have been really busy. When I have some time, I will sit down and figure out the best way to proceed based on the comments above.

wconrad265 avatar Mar 19 '25 14:03 wconrad265

In fact, this feature was turned off(#39) due to its complexity.

Since many people later needed this feature, I think it can be redesigned.

I have some suggestions:

  1. In OpenAPI (app instance), adding validate_response can propagated to all APIBlueprint and APIs decorated by the app.
  2. In APIBlueprint, adding validate_response can be propagated to the APIs decorated with APIBlueprint.
  3. Add validate_response in the API, applying it to a single API.

@wconrad265 welcome to submit PR.

@ddorian @mr-tabasco Hope we can work together to achieve this feature.

I started looking at the code last night and just began working on this problem not too long ago.

Hoping to have a rough draft of changes up later today or tomorrow (depending on whether my kids will let me focus).

I'll leave some notes/comments on the change set that I'd like someone to look through and perhaps leave comments on the final direction of things, so don't worry about it being littered with verbose comments for now, they'll obviously be cleaned up before bringing them in - assuming the changes are sound.

mr-tabasco avatar May 10 '25 15:05 mr-tabasco

Would it be possible to keep the similar type syntax that body/path/query has?

class BookBodyWithID(BaseModel):
    bid: int = Field(..., description='book id')
    age: Optional[int] = Field(None, ge=2, le=4, description='Age')
    author: str = Field(None, min_length=2, max_length=4, description='Author')


class BookResponse(BaseModel):
    code: int = Field(0, description="status code")
    message: str = Field("ok", description="exception information")
    data: BookBodyWithID


@app.get('/book/<int:bid>', 
         tags=[book_tag])
def get_book(path: BookPath, query: BookBody) -> BookResponse:
    """get a book
    get book by id, age or author
    """
    return {"code": 0, "message": "ok", "data": {}}

That way if the type is unset, it's effectively "disabled" but if it is set, then we validate it.

lowlandghost avatar May 17 '25 04:05 lowlandghost