diff --git a/REQUIRED_NONE_ENHANCEMENT.md b/REQUIRED_NONE_ENHANCEMENT.md index ea0224876..8f38c799f 100644 --- a/REQUIRED_NONE_ENHANCEMENT.md +++ b/REQUIRED_NONE_ENHANCEMENT.md @@ -12,7 +12,7 @@ Users often want to create APIs where: 3. Validation rules still apply when the parameter is not None The existing documentation shows either: -- Optional parameters: `param: str | None = None` +- Optional parameters: `param: str | None = None` - Required parameters: `param: str` But doesn't clearly demonstrate the middle ground: **required but nullable**. @@ -32,7 +32,7 @@ async def read_items( # Handle explicit None case if q == "null": q = None - + if q is not None: # Filter logic here return {"items": [...], "filtered_by": q} @@ -44,14 +44,14 @@ async def read_items( ### Key Points 1. **No default value** - Makes the parameter required -2. **Union[str, None]** - Allows both string and None values +2. **Union[str, None]** - Allows both string and None values 3. **Handle "null" string** - Convert string "null" to Python None 4. **Validation applies** - min_length=3 still enforced for non-None values ### Usage Examples ```bash -# ✅ Valid - searches for "python" +# ✅ Valid - searches for "python" GET /items/?q=python # ✅ Valid - explicitly no filtering (null converted to None) @@ -70,7 +70,7 @@ GET /items/?q=ab - `tutorial016_required_can_be_none_py310.py` - Simple demonstration - `tutorial016_required_can_be_none_an.py` - Compatible with older Python versions -### Comprehensive Example +### Comprehensive Example - `tutorial017_comprehensive_required_none_py310.py` - Advanced patterns including: - Multiple required parameters with different types - Custom validation and error handling @@ -80,7 +80,7 @@ GET /items/?q=ab ## Why This Enhancement Matters 1. **Addresses Real User Need** - Issue #12419 shows this is a genuine pain point -2. **Fills Documentation Gap** - Current docs don't cover this specific pattern +2. **Fills Documentation Gap** - Current docs don't cover this specific pattern 3. **Provides Working Code** - Complete, tested examples ready to use 4. **Shows Best Practices** - Error handling, validation, and API design patterns @@ -97,7 +97,7 @@ All examples include comprehensive test coverage showing: This enhancement connects to existing FastAPI documentation: - Query Parameters tutorial - Parameter validation -- Request validation and error handling +- Request validation and error handling - Type hints and annotations The examples follow FastAPI's established patterns while addressing the specific "required but nullable" use case. @@ -109,4 +109,4 @@ Expected to help: - Developers migrating from other frameworks with different parameter semantics - API designers needing explicit null/empty state handling -This addresses a frequently asked question and provides clear, working solutions with comprehensive examples. \ No newline at end of file +This addresses a frequently asked question and provides clear, working solutions with comprehensive examples. diff --git a/docs_src/query_params_str_validations/tutorial016_required_can_be_none_an.py b/docs_src/query_params_str_validations/tutorial016_required_can_be_none_an.py index 8da7aa2c1..c121f9807 100644 --- a/docs_src/query_params_str_validations/tutorial016_required_can_be_none_an.py +++ b/docs_src/query_params_str_validations/tutorial016_required_can_be_none_an.py @@ -7,29 +7,27 @@ app = FastAPI() @app.get("/items/") -async def read_items( - q: Annotated[Union[str, None], Query(min_length=3)] = ... -): +async def read_items(q: Annotated[Union[str, None], Query(min_length=3)] = ...): """ Example of a required query parameter that can be None. - + The parameter 'q' is required (must be provided by the client) but can explicitly be set to None by passing "null" as string. - + - If q is provided and valid: returns items filtered by q - If q is "null": treats it as None and returns all items - If q is missing: returns 422 validation error (required parameter) - If q is too short: returns 422 validation error (min_length=3) """ results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - + # Handle explicit None case if q == "null": q = None - + if q is not None: results.update({"q": q, "filtered": True}) else: results.update({"q": None, "filtered": False}) - - return results \ No newline at end of file + + return results diff --git a/docs_src/query_params_str_validations/tutorial016_required_can_be_none_py310.py b/docs_src/query_params_str_validations/tutorial016_required_can_be_none_py310.py index 4a3f547ac..ba4b9f6d2 100644 --- a/docs_src/query_params_str_validations/tutorial016_required_can_be_none_py310.py +++ b/docs_src/query_params_str_validations/tutorial016_required_can_be_none_py310.py @@ -6,29 +6,27 @@ app = FastAPI() @app.get("/items/") -async def read_items( - q: Annotated[Union[str, None], Query(min_length=3)] -): +async def read_items(q: Annotated[Union[str, None], Query(min_length=3)]): """ Example of a required query parameter that can be None. - + The parameter 'q' is required (must be provided by the client) but can explicitly be set to None by passing "null" as string. - + - If q is provided and valid: returns items filtered by q - If q is "null": treats it as None and returns all items - If q is missing: returns 422 validation error (required parameter) - If q is too short: returns 422 validation error (min_length=3) """ results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - + # Handle explicit None case if q == "null": q = None - + if q is not None: results.update({"q": q, "filtered": True}) else: results.update({"q": None, "filtered": False}) - - return results \ No newline at end of file + + return results diff --git a/docs_src/query_params_str_validations/tutorial017_comprehensive_required_none_py310.py b/docs_src/query_params_str_validations/tutorial017_comprehensive_required_none_py310.py index b1a26dfcb..01acc902e 100644 --- a/docs_src/query_params_str_validations/tutorial017_comprehensive_required_none_py310.py +++ b/docs_src/query_params_str_validations/tutorial017_comprehensive_required_none_py310.py @@ -1,17 +1,15 @@ from typing import Annotated, Union -from fastapi import FastAPI, Query, HTTPException +from fastapi import FastAPI, HTTPException, Query app = FastAPI() @app.get("/items/basic/") -async def read_items_basic( - q: Annotated[Union[str, None], Query(min_length=3)] -): +async def read_items_basic(q: Annotated[Union[str, None], Query(min_length=3)]): """ Basic example: Required query parameter that can be None. - + Usage examples: - GET /items/basic/?q=test -> filters by "test" - GET /items/basic/?q=null -> returns all items (q treated as None) @@ -19,85 +17,83 @@ async def read_items_basic( - GET /items/basic/?q=ab -> 422 error (too short) """ results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - + # Handle explicit None case if q == "null": q = None - + if q is not None: results.update({"q": q, "filtered": True}) else: results.update({"q": None, "filtered": False}) - + return results @app.get("/items/advanced/") async def read_items_advanced( q: Annotated[Union[str, None], Query(min_length=3)], - include_metadata: Annotated[bool, Query()] = False + include_metadata: Annotated[bool, Query()] = False, ): """ Advanced example: Multiple required parameters with None handling. - + This shows how to handle multiple query parameters where some can be None and others have different types. - + Usage examples: - GET /items/advanced/?q=search&include_metadata=true - - GET /items/advanced/?q=null&include_metadata=false + - GET /items/advanced/?q=null&include_metadata=false - GET /items/advanced/?q=search (include_metadata defaults to false) """ # Convert "null" string to actual None if q == "null": q = None - + # Simulate database items all_items = [ {"item_id": "Foo", "tags": ["tag1", "tag2"], "active": True}, {"item_id": "Bar", "tags": ["tag3"], "active": False}, - {"item_id": "Baz", "tags": ["tag1"], "active": True} + {"item_id": "Baz", "tags": ["tag1"], "active": True}, ] - + # Filter items if query is provided if q is not None: filtered_items = [ - item for item in all_items - if q.lower() in item["item_id"].lower() or - any(q.lower() in tag.lower() for tag in item["tags"]) + item + for item in all_items + if q.lower() in item["item_id"].lower() + or any(q.lower() in tag.lower() for tag in item["tags"]) ] else: filtered_items = all_items - + # Remove metadata if not requested if not include_metadata: - filtered_items = [ - {"item_id": item["item_id"]} - for item in filtered_items - ] - + filtered_items = [{"item_id": item["item_id"]} for item in filtered_items] + return { "items": filtered_items, "query_used": q, "total_found": len(filtered_items), - "metadata_included": include_metadata + "metadata_included": include_metadata, } @app.get("/items/validation/") async def read_items_with_custom_validation( - q: Annotated[Union[str, None], Query(min_length=3)] + q: Annotated[Union[str, None], Query(min_length=3)], ): """ Example with custom validation and error handling. - + This shows how to add custom business logic validation while still using FastAPI's automatic validation. """ # Handle explicit None case if q == "null": q = None - + # Custom validation (after FastAPI's automatic validation) if q is not None: # Example: Reject certain reserved words @@ -105,25 +101,29 @@ async def read_items_with_custom_validation( if q.lower() in reserved_words: raise HTTPException( status_code=400, - detail=f"Query '{q}' is a reserved word and cannot be used for searching" + detail=f"Query '{q}' is a reserved word and cannot be used for searching", ) - + # Example: Convert to proper search format q = q.strip().lower() - + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - + if q is not None: - results.update({ - "q": q, - "search_performed": True, - "note": "Query has been normalized to lowercase" - }) + results.update( + { + "q": q, + "search_performed": True, + "note": "Query has been normalized to lowercase", + } + ) else: - results.update({ - "q": None, - "search_performed": False, - "note": "No search query provided (null value received)" - }) - - return results \ No newline at end of file + results.update( + { + "q": None, + "search_performed": False, + "note": "No search query provided (null value received)", + } + ) + + return results