Response Data Validation in flask-openapi3
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.
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:
- In
OpenAPI(app instance), addingvalidate_responsecan propagated to allAPIBlueprintand APIs decorated by the app. - In
APIBlueprint, addingvalidate_responsecan be propagated to the APIs decorated withAPIBlueprint. - Add
validate_responsein 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 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.
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.
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:
- In
OpenAPI(app instance), addingvalidate_responsecan propagated to allAPIBlueprintand APIs decorated by the app.- In
APIBlueprint, addingvalidate_responsecan be propagated to the APIs decorated withAPIBlueprint.- Add
validate_responsein 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.
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.