Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow lists to be used as models for validate_request and validate_response #67

Closed
wants to merge 4 commits into from

Conversation

bselman1
Copy link
Contributor

Addresses Issue #16.

Change how validate_request and validate_response validate if the provided model is the same as the model provided in the decorator. For BaseModel and RootModel instances, we can use the validate_python method to do the validation. In all other cases, we'll instead use a pydantic TypeAdapter which will attempt to build a validator for the requested model.

These changes allow us to use the following methods for declaring a method that expects a list of items:

import asyncio
from pydantic import BaseModel, RootModel
from quart import Quart
from quart_schema import QuartSchema, validate_request
from typing import Optional

class Details(BaseModel):
    name: str
    age: Optional[int] = None

class Item(BaseModel):
    count: int
    details: Details

class ItemCollection(RootModel):
    root: list[Item]

def create_app():
    app = Quart(__name__)
    quart_schema = QuartSchema(
        app, 
        swagger_ui_path = '/api/docs',
        openapi_path = '/api/openapi.json',
        redoc_ui_path = None
    )

    @app.route("/items", methods = ["POST"])
    @validate_request(ItemCollection)
    async def handle_items_collection(data: ItemCollection):
        return f"{type(data)}"
    
    @app.route("/items-list", methods = ["POST"])
    @validate_request(list[Item])
    async def handle_items_list(data: list[Item]):
        return f"{type(data)}"
    
    @app.route("/items-root-model", methods = ["POST"])
    @validate_request(RootModel[list[Item]])
    async def handle_items_root_model(data: RootModel[list[Item]]):
        return f"{type(data)}"
    
    return app

async def do_tests(app: Quart):
    items = [
        { "count": 2, "details": { "name": "bob" } },
        { "count": 2, "details": { "name": "jane" } }
    ]
    test_client = app.test_client()
    response = await test_client.post("/items", json=items)
    assert response.status_code == 200
    assert "<class '__main__.ItemCollection'>" == await response.get_data(as_text = True)

    test_client = app.test_client()
    response = await test_client.post("/items-list", json=items)
    assert response.status_code == 200
    assert "<class 'list'>" == await response.get_data(as_text = True)

    test_client = app.test_client()
    response = await test_client.post("/items-root-model", json=items)
    assert response.status_code == 200
    assert "<class 'pydantic.root_model.RootModel[list[Item]]'>" == await response.get_data(as_text = True)

def main():
    app = create_app()
    asyncio.run(do_tests(app))
    # Uncomment if you want to actually start the web server to view the documented API
    ## app.run(debug = True)

if __name__ ==  '__main__':
    main()

Additional tests have been added to validate cases where lists or RootModels are passed to validate_request and validate_response.

Note that this PR doesn't make any changes to the header validation or query string validation. I wanted to get an idea if this altered way of validating makes sense before making too many changes.

Current commit will fail when trying to validate a response that returns
a List[T] or a RootModel[T].
Fixes the problem with response validators that return either a list[T]
or a RootModel[T]. We now build a separate model_validator method at
the top of the ```validate_response``` decorator method that will use
the ```model_validate``` method if the model class is a pydantic
```RootModel``` or ```BaseModel``` instance. Otherwise, create a pydantic
```TypeAdapter``` that will attempt to build the model class from the
provided python data object.
Make use of a new model_validator method that will include RootModel
instances in addtion to the existing BaseModel, dataclass, or pydantic
dataclass.
@pgjones
Copy link
Owner

pgjones commented Jan 22, 2024

Thanks, I've gone for a very similar approach. I've not documented this yet, as it requires further testing, but please give it a go and let me know your thoughts.

@pgjones pgjones closed this Jan 22, 2024
@bselman1 bselman1 deleted the bselman1/enhance-validation branch May 15, 2024 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants