[{"data":1,"prerenderedAt":2521},["ShallowReactive",2],{"nav":3,"page-\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fquery-path-and-body-parameter-validation\u002F":580,"surround-\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fquery-path-and-body-parameter-validation\u002F":2520},[4,186,386],{"title":5,"path":6,"stem":7,"children":8},"Advanced Pydantic Validation Serialization","\u002Fadvanced-pydantic-validation-serialization","advanced-pydantic-validation-serialization",[9,12,42,66,90,114,150,174],{"title":10,"path":6,"stem":11},"Advanced Pydantic Validation and Serialization","advanced-pydantic-validation-serialization\u002Findex",{"title":13,"path":14,"stem":15,"children":16},"Custom Validators and Field Constraints in Pydantic","\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints","advanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Findex",[17,18,24,30,36],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":22},"Before, After and Wrap Validators in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fbefore-after-and-wrap-validators","advanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fbefore-after-and-wrap-validators\u002Findex",[23],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":28},"Creating Reusable Custom Validators in Pydantic","\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fcreating-reusable-custom-validators-in-pydantic","advanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fcreating-reusable-custom-validators-in-pydantic\u002Findex",[29],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":34},"Cross-Field Validation Patterns in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fcross-field-validation-patterns","advanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fcross-field-validation-patterns\u002Findex",[35],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":40},"Pydantic v2 Async Custom Validator: What to Do Instead","\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fpydantic-v2-async-custom-validator","advanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fpydantic-v2-async-custom-validator\u002Findex",[41],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":46},"JSON Schema Customization in Pydantic and FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization","advanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Findex",[47,48,54,60],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":52},"Customizing OpenAPI Schema Generation in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fcustomizing-openapi-schema-generation-in-fastapi","advanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fcustomizing-openapi-schema-generation-in-fastapi\u002Findex",[53],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":58},"Discriminated Unions in OpenAPI with Pydantic","\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fdiscriminated-unions-in-openapi","advanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fdiscriminated-unions-in-openapi\u002Findex",[59],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":64},"Examples in the OpenAPI Schema with FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fexamples-in-openapi-schema","advanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fexamples-in-openapi-schema\u002Findex",[65],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":70},"Nested Model Serialization in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fnested-model-serialization","advanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Findex",[71,72,78,84],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":76},"Excluding Fields Per Endpoint in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fexcluding-fields-per-endpoint","advanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fexcluding-fields-per-endpoint\u002Findex",[77],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":82},"Handling Deeply Nested JSON Models Efficiently","\u002Fadvanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fhandling-deeply-nested-json-models-efficiently","advanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fhandling-deeply-nested-json-models-efficiently\u002Findex",[83],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87,"children":88},"Self-Referencing and Recursive Models in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fself-referencing-and-recursive-models","advanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fself-referencing-and-recursive-models\u002Findex",[89],{"title":85,"path":86,"stem":87},{"title":91,"path":92,"stem":93,"children":94},"Performance Optimization for Pydantic Models in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models","advanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Findex",[95,96,102,108],{"title":91,"path":92,"stem":93},{"title":97,"path":98,"stem":99,"children":100},"model_construct and When to Skip Validation in Pydantic","\u002Fadvanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Fmodel-construct-when-to-skip-validation","advanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Fmodel-construct-when-to-skip-validation\u002Findex",[101],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":106},"Pydantic Model Serialization Performance in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Fpydantic-model-serialization-performance","advanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Fpydantic-model-serialization-performance\u002Findex",[107],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":112},"TypeAdapter for Non-Model Types in Pydantic","\u002Fadvanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Ftypeadapter-for-non-model-types","advanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Ftypeadapter-for-non-model-types\u002Findex",[113],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":118},"Pydantic V2 Migration Guide for FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Findex",[119,120,126,132,138,144],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":124},"Migrate @validator to @field_validator in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmigrate-validator-to-field-validator","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmigrate-validator-to-field-validator\u002Findex",[125],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":130},"Migrating from Pydantic v1 to v2 Without Breaking APIs","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmigrating-from-pydantic-v1-to-v2-without-breaking-apis","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmigrating-from-pydantic-v1-to-v2-without-breaking-apis\u002Findex",[131],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":136},"model_config vs class Config in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmodel-config-vs-class-config","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmodel-config-vs-class-config\u002Findex",[137],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":142},"Replacing json_encoders with field_serializer in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Freplacing-json-encoders-with-field-serializer","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Freplacing-json-encoders-with-field-serializer\u002Findex",[143],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":148},"Migrating @root_validator to @model_validator in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Froot-validator-to-model-validator","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Froot-validator-to-model-validator\u002Findex",[149],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":154},"Request Validation Patterns in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns","advanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Findex",[155,156,162,168],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":160},"Optional vs Nullable Fields in Pydantic and FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Foptional-vs-nullable-fields","advanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Foptional-vs-nullable-fields\u002Findex",[161],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":166},"Query, Path and Body Parameter Validation in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fquery-path-and-body-parameter-validation","advanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fquery-path-and-body-parameter-validation\u002Findex",[167],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":172},"Validating File Uploads and Forms in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fvalidating-file-uploads-and-forms","advanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fvalidating-file-uploads-and-forms\u002Findex",[173],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":178},"Type Hinting and IDE Integration in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Ftype-hinting-ide-integration","advanced-pydantic-validation-serialization\u002Ftype-hinting-ide-integration\u002Findex",[179,180],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":184},"Annotated Dependencies and Reusable Types in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Ftype-hinting-ide-integration\u002Fannotated-dependencies-and-reusable-types","advanced-pydantic-validation-serialization\u002Ftype-hinting-ide-integration\u002Fannotated-dependencies-and-reusable-types\u002Findex",[185],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":190},"Async Background Tasks Observability","\u002Fasync-background-tasks-observability","async-background-tasks-observability",[191,194,224,254,284,308,338,362],{"title":192,"path":188,"stem":193},"Async, Background Tasks, and Observability in FastAPI","async-background-tasks-observability\u002Findex",{"title":195,"path":196,"stem":197,"children":198},"Async Correctness and Concurrency in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Findex",[199,200,206,212,218],{"title":195,"path":196,"stem":197},{"title":201,"path":202,"stem":203,"children":204},"Concurrent Requests with asyncio.gather in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Fconcurrent-requests-with-asyncio-gather","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Fconcurrent-requests-with-asyncio-gather\u002Findex",[205],{"title":201,"path":202,"stem":203},{"title":207,"path":208,"stem":209,"children":210},"FastAPI async def vs def: Performance and When to Use Each","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffastapi-async-def-vs-def-performance","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffastapi-async-def-vs-def-performance\u002Findex",[211],{"title":207,"path":208,"stem":209},{"title":213,"path":214,"stem":215,"children":216},"Fixing Blocking Calls in Async FastAPI Routes","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffixing-blocking-calls-in-async-routes","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffixing-blocking-calls-in-async-routes\u002Findex",[217],{"title":213,"path":214,"stem":215},{"title":219,"path":220,"stem":221,"children":222},"Running Sync Code in a Threadpool in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Frunning-sync-code-in-a-threadpool","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Frunning-sync-code-in-a-threadpool\u002Findex",[223],{"title":219,"path":220,"stem":221},{"title":225,"path":226,"stem":227,"children":228},"Async Database Sessions in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions","async-background-tasks-observability\u002Fasync-database-sessions\u002Findex",[229,230,236,242,248],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":234},"Async SQLAlchemy Session per Request in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Fasync-sqlalchemy-session-per-request","async-background-tasks-observability\u002Fasync-database-sessions\u002Fasync-sqlalchemy-session-per-request\u002Findex",[235],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":240},"Fixing asyncpg Connection Pool Exhaustion in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ffixing-asyncpg-pool-exhaustion","async-background-tasks-observability\u002Fasync-database-sessions\u002Ffixing-asyncpg-pool-exhaustion\u002Findex",[241],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":246},"Testing with Async Database Fixtures in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ftesting-with-async-database-fixtures","async-background-tasks-observability\u002Fasync-database-sessions\u002Ftesting-with-async-database-fixtures\u002Findex",[247],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":252},"Transaction Management and Rollback in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ftransaction-management-and-rollback","async-background-tasks-observability\u002Fasync-database-sessions\u002Ftransaction-management-and-rollback\u002Findex",[253],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":258},"Background Task Processing in FastAPI","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing","async-background-tasks-observability\u002Fbackground-task-processing\u002Findex",[259,260,266,272,278],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":264},"FastAPI BackgroundTasks vs Celery vs ARQ","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002Ffastapi-backgroundtasks-vs-celery-vs-arq","async-background-tasks-observability\u002Fbackground-task-processing\u002Ffastapi-backgroundtasks-vs-celery-vs-arq\u002Findex",[265],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":270},"Retry and Idempotency for FastAPI Background Tasks","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002Fretry-and-idempotency-for-tasks","async-background-tasks-observability\u002Fbackground-task-processing\u002Fretry-and-idempotency-for-tasks\u002Findex",[271],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":276},"Running ARQ Workers with FastAPI","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002Frunning-arq-workers-with-fastapi","async-background-tasks-observability\u002Fbackground-task-processing\u002Frunning-arq-workers-with-fastapi\u002Findex",[277],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":282},"When FastAPI BackgroundTasks Silently Fails","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002Fwhen-backgroundtasks-silently-fails","async-background-tasks-observability\u002Fbackground-task-processing\u002Fwhen-backgroundtasks-silently-fails\u002Findex",[283],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":288},"Caching Strategies in FastAPI","\u002Fasync-background-tasks-observability\u002Fcaching-strategies","async-background-tasks-observability\u002Fcaching-strategies\u002Findex",[289,290,296,302],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":294},"Cache Invalidation Patterns in FastAPI","\u002Fasync-background-tasks-observability\u002Fcaching-strategies\u002Fcache-invalidation-patterns-in-fastapi","async-background-tasks-observability\u002Fcaching-strategies\u002Fcache-invalidation-patterns-in-fastapi\u002Findex",[295],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":300},"Caching Dependency Results in FastAPI","\u002Fasync-background-tasks-observability\u002Fcaching-strategies\u002Fcaching-dependency-results","async-background-tasks-observability\u002Fcaching-strategies\u002Fcaching-dependency-results\u002Findex",[301],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":306},"Redis Response Caching in FastAPI","\u002Fasync-background-tasks-observability\u002Fcaching-strategies\u002Fredis-response-caching-in-fastapi","async-background-tasks-observability\u002Fcaching-strategies\u002Fredis-response-caching-in-fastapi\u002Findex",[307],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":312},"Observability and Tracing in FastAPI","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing","async-background-tasks-observability\u002Fobservability-and-tracing\u002Findex",[313,314,320,326,332],{"title":309,"path":310,"stem":311},{"title":315,"path":316,"stem":317,"children":318},"Correlating Logs, Traces and Errors in FastAPI","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002Fcorrelating-logs-traces-and-errors","async-background-tasks-observability\u002Fobservability-and-tracing\u002Fcorrelating-logs-traces-and-errors\u002Findex",[319],{"title":315,"path":316,"stem":317},{"title":321,"path":322,"stem":323,"children":324},"Instrumenting FastAPI with OpenTelemetry","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002Finstrumenting-fastapi-with-opentelemetry","async-background-tasks-observability\u002Fobservability-and-tracing\u002Finstrumenting-fastapi-with-opentelemetry\u002Findex",[325],{"title":321,"path":322,"stem":323},{"title":327,"path":328,"stem":329,"children":330},"Prometheus Metrics for FastAPI","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002Fprometheus-metrics-for-fastapi","async-background-tasks-observability\u002Fobservability-and-tracing\u002Fprometheus-metrics-for-fastapi\u002Findex",[331],{"title":327,"path":328,"stem":329},{"title":333,"path":334,"stem":335,"children":336},"Structured JSON Logging with Request IDs in FastAPI","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002Fstructured-json-logging-with-request-ids","async-background-tasks-observability\u002Fobservability-and-tracing\u002Fstructured-json-logging-with-request-ids\u002Findex",[337],{"title":333,"path":334,"stem":335},{"title":339,"path":340,"stem":341,"children":342},"Rate Limiting and Throttling in FastAPI","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling","async-background-tasks-observability\u002Frate-limiting-throttling\u002Findex",[343,344,350,356],{"title":339,"path":340,"stem":341},{"title":345,"path":346,"stem":347,"children":348},"FastAPI Rate Limiting with Redis and SlowAPI","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Ffastapi-rate-limiting-with-redis-slowapi","async-background-tasks-observability\u002Frate-limiting-throttling\u002Ffastapi-rate-limiting-with-redis-slowapi\u002Findex",[349],{"title":345,"path":346,"stem":347},{"title":351,"path":352,"stem":353,"children":354},"Per-User Token Bucket Throttling in FastAPI","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Fper-user-token-bucket-throttling","async-background-tasks-observability\u002Frate-limiting-throttling\u002Fper-user-token-bucket-throttling\u002Findex",[355],{"title":351,"path":352,"stem":353},{"title":357,"path":358,"stem":359,"children":360},"Rate Limit Headers and 429 Responses in FastAPI","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Frate-limit-headers-and-429-responses","async-background-tasks-observability\u002Frate-limiting-throttling\u002Frate-limit-headers-and-429-responses\u002Findex",[361],{"title":357,"path":358,"stem":359},{"title":363,"path":364,"stem":365,"children":366},"Testing FastAPI Applications","\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications","async-background-tasks-observability\u002Ftesting-fastapi-applications\u002Findex",[367,368,374,380],{"title":363,"path":364,"stem":365},{"title":369,"path":370,"stem":371,"children":372},"Mocking External Services in FastAPI Tests","\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Fmocking-external-services-in-tests","async-background-tasks-observability\u002Ftesting-fastapi-applications\u002Fmocking-external-services-in-tests\u002Findex",[373],{"title":369,"path":370,"stem":371},{"title":375,"path":376,"stem":377,"children":378},"TestClient vs httpx AsyncClient in FastAPI","\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftestclient-vs-httpx-asyncclient","async-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftestclient-vs-httpx-asyncclient\u002Findex",[379],{"title":375,"path":376,"stem":377},{"title":381,"path":382,"stem":383,"children":384},"Testing Async FastAPI Endpoints with pytest-asyncio","\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftesting-async-endpoints-with-pytest-asyncio","async-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftesting-async-endpoints-with-pytest-asyncio\u002Findex",[385],{"title":381,"path":382,"stem":383},{"title":387,"path":388,"stem":389,"children":390},"Core Architecture Routing Patterns","\u002Fcore-architecture-routing-patterns","core-architecture-routing-patterns",[391,394,412,436,472,496,526,556],{"title":392,"path":388,"stem":393},"FastAPI Core Architecture and Routing Patterns","core-architecture-routing-patterns\u002Findex",{"title":395,"path":396,"stem":397,"children":398},"Application Factory Patterns in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fapplication-factory-patterns","core-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Findex",[399,400,406],{"title":395,"path":396,"stem":397},{"title":401,"path":402,"stem":403,"children":404},"FastAPI App Factory Pattern for Testing and Deployment","\u002Fcore-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Ffastapi-app-factory-pattern-for-testing-and-deployment","core-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Ffastapi-app-factory-pattern-for-testing-and-deployment\u002Findex",[405],{"title":401,"path":402,"stem":403},{"title":407,"path":408,"stem":409,"children":410},"Lifespan Events vs Startup and Shutdown in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Flifespan-events-vs-startup-shutdown","core-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Flifespan-events-vs-startup-shutdown\u002Findex",[411],{"title":407,"path":408,"stem":409},{"title":413,"path":414,"stem":415,"children":416},"Configuration Management in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fconfiguration-management","core-architecture-routing-patterns\u002Fconfiguration-management\u002Findex",[417,418,424,430],{"title":413,"path":414,"stem":415},{"title":419,"path":420,"stem":421,"children":422},"Managing Environment Variables with Pydantic Settings","\u002Fcore-architecture-routing-patterns\u002Fconfiguration-management\u002Fmanaging-environment-variables-with-pydantic-settings","core-architecture-routing-patterns\u002Fconfiguration-management\u002Fmanaging-environment-variables-with-pydantic-settings\u002Findex",[423],{"title":419,"path":420,"stem":421},{"title":425,"path":426,"stem":427,"children":428},"Pydantic Settings vs Dynaconf vs python-decouple","\u002Fcore-architecture-routing-patterns\u002Fconfiguration-management\u002Fpydantic-settings-vs-dynaconf-vs-python-decouple","core-architecture-routing-patterns\u002Fconfiguration-management\u002Fpydantic-settings-vs-dynaconf-vs-python-decouple\u002Findex",[429],{"title":425,"path":426,"stem":427},{"title":431,"path":432,"stem":433,"children":434},"Secrets and .env Files Per Environment in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fconfiguration-management\u002Fsecrets-and-env-files-per-environment","core-architecture-routing-patterns\u002Fconfiguration-management\u002Fsecrets-and-env-files-per-environment\u002Findex",[435],{"title":431,"path":432,"stem":433},{"title":437,"path":438,"stem":439,"children":440},"Dependency Injection Strategies in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Findex",[441,442,448,454,460,466],{"title":437,"path":438,"stem":439},{"title":443,"path":444,"stem":445,"children":446},"Best Practices for FastAPI Dependency Injection","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fbest-practices-for-fastapi-dependency-injection","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fbest-practices-for-fastapi-dependency-injection\u002Findex",[447],{"title":443,"path":444,"stem":445},{"title":449,"path":450,"stem":451,"children":452},"Dependency Caching and use_cache in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fdependency-caching-and-use-cache","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fdependency-caching-and-use-cache\u002Findex",[453],{"title":449,"path":450,"stem":451},{"title":455,"path":456,"stem":457,"children":458},"Fixing FastAPI Dependency Injection Circular Imports","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Ffastapi-dependency-injection-circular-import-fix","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Ffastapi-dependency-injection-circular-import-fix\u002Findex",[459],{"title":455,"path":456,"stem":457},{"title":461,"path":462,"stem":463,"children":464},"Overriding Dependencies in FastAPI Tests","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Foverriding-dependencies-in-tests","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Foverriding-dependencies-in-tests\u002Findex",[465],{"title":461,"path":462,"stem":463},{"title":467,"path":468,"stem":469,"children":470},"Yield Dependencies and Cleanup Order in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fyield-dependencies-and-cleanup-order","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fyield-dependencies-and-cleanup-order\u002Findex",[471],{"title":467,"path":468,"stem":469},{"title":473,"path":474,"stem":475,"children":476},"Error Handling and Global Exceptions in FastAPI","\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions","core-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Findex",[477,478,484,490],{"title":473,"path":474,"stem":475},{"title":479,"path":480,"stem":481,"children":482},"Customising Validation Error Responses in FastAPI","\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fcustomising-validation-error-responses","core-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fcustomising-validation-error-responses\u002Findex",[483],{"title":479,"path":480,"stem":481},{"title":485,"path":486,"stem":487,"children":488},"Global Exception Handlers for Consistent API Responses","\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fglobal-exception-handlers-for-consistent-api-responses","core-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fglobal-exception-handlers-for-consistent-api-responses\u002Findex",[489],{"title":485,"path":486,"stem":487},{"title":491,"path":492,"stem":493,"children":494},"HTTPException vs Custom Exception Classes in FastAPI","\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fhttpexception-vs-custom-exception-classes","core-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fhttpexception-vs-custom-exception-classes\u002Findex",[495],{"title":491,"path":492,"stem":493},{"title":497,"path":498,"stem":499,"children":500},"Middleware Implementation in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Findex",[501,502,508,514,520],{"title":497,"path":498,"stem":499},{"title":503,"path":504,"stem":505,"children":506},"CORS Middleware Configuration in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fcors-middleware-configuration","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fcors-middleware-configuration\u002Findex",[507],{"title":503,"path":504,"stem":505},{"title":509,"path":510,"stem":511,"children":512},"Implementing Custom Middleware for Request Tracing","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fimplementing-custom-middleware-for-request-tracing","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fimplementing-custom-middleware-for-request-tracing\u002Findex",[513],{"title":509,"path":510,"stem":511},{"title":515,"path":516,"stem":517,"children":518},"Middleware Execution Order in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fmiddleware-execution-order","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fmiddleware-execution-order\u002Findex",[519],{"title":515,"path":516,"stem":517},{"title":521,"path":522,"stem":523,"children":524},"Middleware vs Dependencies: When to Use Which","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fmiddleware-vs-dependencies-when-to-use-which","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fmiddleware-vs-dependencies-when-to-use-which\u002Findex",[525],{"title":521,"path":522,"stem":523},{"title":527,"path":528,"stem":529,"children":530},"Modular Router Organization in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Findex",[531,532,538,544,550],{"title":527,"path":528,"stem":529},{"title":533,"path":534,"stem":535,"children":536},"APIRouter Prefix vs Sub-Application Mounting in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization\u002Fapirouter-prefix-vs-sub-application-mounting","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Fapirouter-prefix-vs-sub-application-mounting\u002Findex",[537],{"title":533,"path":534,"stem":535},{"title":539,"path":540,"stem":541,"children":542},"How to Structure Large FastAPI Projects for Scale","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization\u002Fhow-to-structure-large-fastapi-projects-for-scale","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Fhow-to-structure-large-fastapi-projects-for-scale\u002Findex",[543],{"title":539,"path":540,"stem":541},{"title":545,"path":546,"stem":547,"children":548},"Router Tags and OpenAPI Grouping in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization\u002Frouter-tags-and-openapi-grouping","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Frouter-tags-and-openapi-grouping\u002Findex",[549],{"title":545,"path":546,"stem":547},{"title":551,"path":552,"stem":553,"children":554},"Versioning APIs with FastAPI Routers","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization\u002Fversioning-apis-with-routers","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Fversioning-apis-with-routers\u002Findex",[555],{"title":551,"path":552,"stem":553},{"title":557,"path":558,"stem":559,"children":560},"The FastAPI Request\u002FResponse Lifecycle","\u002Fcore-architecture-routing-patterns\u002Frequest-response-lifecycle","core-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Findex",[561,562,568,574],{"title":557,"path":558,"stem":559},{"title":563,"path":564,"stem":565,"children":566},"How a Request Flows Through FastAPI","\u002Fcore-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fhow-a-request-flows-through-fastapi","core-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fhow-a-request-flows-through-fastapi\u002Findex",[567],{"title":563,"path":564,"stem":565},{"title":569,"path":570,"stem":571,"children":572},"Response Model and Serialization Order","\u002Fcore-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fresponse-model-and-serialization-order","core-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fresponse-model-and-serialization-order\u002Findex",[573],{"title":569,"path":570,"stem":571},{"title":575,"path":576,"stem":577,"children":578},"Streaming and File Responses in FastAPI","\u002Fcore-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fstreaming-and-file-responses","core-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fstreaming-and-file-responses\u002Findex",[579],{"title":575,"path":576,"stem":577},{"id":581,"title":163,"body":582,"dateModified":2488,"datePublished":2488,"description":2489,"extension":2490,"faq":2491,"howto":2504,"meta":2505,"navigation":1781,"path":164,"seo":2517,"stem":165,"type":2518,"__hash__":2519},"content\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fquery-path-and-body-parameter-validation\u002Findex.md",{"type":583,"value":584,"toc":2472},"minimark",[585,589,596,671,684,691,831,836,855,862,866,922,936,940,943,1103,1106,1113,1128,1131,1135,1138,1332,1335,1341,1354,1365,1370,1484,1490,1518,1533,1537,1636,1642,1657,1663,1667,1673,1822,1828,1845,1849,1852,1887,1995,2001,2020,2035,2039,2042,2184,2187,2270,2281,2285,2297,2319,2332,2342,2346,2358,2374,2394,2400,2415,2419,2468],[586,587,163],"h1",{"id":588},"query-path-and-body-parameter-validation-in-fastapi",[590,591,592],"p",{},[593,594,595],"strong",{},"Key takeaways:",[597,598,599,637,647,653,661],"ul",{},[600,601,602,606,607,610,611,614,615,606,618,606,621,606,624,606,627,606,630,606,633,636],"li",{},[603,604,605],"code",{},"Query",", ",[603,608,609],{},"Path"," and ",[603,612,613],{},"Body"," carry the same Pydantic constraints: ",[603,616,617],{},"gt",[603,619,620],{},"ge",[603,622,623],{},"lt",[603,625,626],{},"le",[603,628,629],{},"min_length",[603,631,632],{},"max_length",[603,634,635],{},"pattern",".",[600,638,639,640,643,644,636],{},"Declaring a parameter as ",[603,641,642],{},"list[T]"," collects a repeated query key; failures report the element index in ",[603,645,646],{},"loc",[600,648,649,652],{},[603,650,651],{},"alias"," replaces the wire name outright — the Python name stops being accepted.",[600,654,655,656,660],{},"One body parameter means the payload ",[657,658,659],"em",{},"is"," the model; two or more means every one gets embedded under its name.",[600,662,663,666,667,670],{},[603,664,665],{},"Literal"," beats a hand-written enum check: it validates, documents itself in OpenAPI, and produces a ",[603,668,669],{},"literal_error"," listing the allowed values.",[590,672,673,674,679,680,683],{},"This page is the hands-on companion to ",[675,676,678],"a",{"href":677},"\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002F","Request Validation Patterns",", which explains ",[657,681,682],{},"how"," FastAPI decides a parameter's source. Here we assume the source is settled and focus on constraining the value.",[590,685,686,687,690],{},"Every transcript below is the real output of ",[603,688,689],{},"_verify\u002Fexamples\u002Fval-query-path-body.py"," executed on FastAPI 0.139.2, Pydantic 2.13.4 and Python 3.12.",[692,693,694,827],"figure",{},[695,696,704,705,704,709,704,713,704,722,704,729,704,734,704,737,704,741,704,744,704,747,704,751,704,754,704,761,704,766,704,768,704,772,704,778,704,783,704,786,704,789,704,791,704,794,704,799,704,804,704,809,704,814,704,817,704,821,704,824],"svg",{"viewBox":697,"role":698,"ariaLabelledBy":699,"xmlns":702,"style":703},"0 0 720 300","img",[700,701],"qpb-title","qpb-desc","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;max-width:720px;margin:2rem 0","\n  ",[706,707,708],"title",{"id":700},"Where each parameter marker reads its value from",[710,711,712],"desc",{"id":701},"A request URL and JSON body are annotated to show that Path reads a URL segment, Query reads the query string including repeated keys and aliases, and Body reads the JSON payload, which is the model itself when there is one body parameter and embedded when there are several.",[714,715],"rect",{"x":716,"y":717,"width":718,"height":719,"rx":720,"style":721},"20","24","200","46","8","fill:#FFFFFF;stroke:#00796B;stroke-width:1.6px",[723,724,728],"text",{"x":725,"y":726,"style":727},"120","44","text-anchor:middle;fill:#00796B;font:700 13px sans-serif","Path(...)",[723,730,733],{"x":725,"y":731,"style":732},"62","text-anchor:middle;fill:currentColor;font:400 11px sans-serif","ge, le, pattern",[714,735],{"x":736,"y":717,"width":718,"height":719,"rx":720,"style":721},"260",[723,738,740],{"x":739,"y":726,"style":727},"360","Query(...)",[723,742,743],{"x":739,"y":731,"style":732},"alias, list, bounds",[714,745],{"x":746,"y":717,"width":718,"height":719,"rx":720,"style":721},"500",[723,748,750],{"x":749,"y":726,"style":727},"600","Body(...)",[723,752,753],{"x":749,"y":731,"style":732},"embed, scalars",[714,755],{"x":716,"y":756,"width":757,"height":758,"rx":759,"style":760},"112","440","40","6","fill:#F9FAFB;stroke:currentColor;stroke-width:1.3px",[723,762,765],{"x":758,"y":763,"style":764},"137","text-anchor:start;fill:currentColor;font:400 12px monospace","\u002Freports\u002F2026\u002Fq2?tag=a&tag=b",[714,767],{"x":746,"y":756,"width":718,"height":758,"rx":759,"style":760},[723,769,771],{"x":770,"y":763,"style":764},"518","{\"code\": \"X\", ...}",[773,774],"line",{"x1":725,"y1":775,"x2":725,"y2":776,"style":777},"70","108","stroke:#00796B;stroke-width:1.5px",[779,780],"polygon",{"points":781,"style":782},"116,104 124,104 120,112","fill:#00796B",[773,784],{"x1":739,"y1":775,"x2":785,"y2":776,"style":777},"330",[779,787],{"points":788,"style":782},"326,102 336,106 330,112",[773,790],{"x1":749,"y1":775,"x2":749,"y2":776,"style":777},[779,792],{"points":793,"style":782},"596,104 604,104 600,112",[714,795],{"x":716,"y":796,"width":785,"height":797,"rx":720,"style":798},"190","72","fill:#E0F2F1;stroke:#00796B;stroke-width:1.5px",[723,800,803],{"x":801,"y":802,"style":727},"185","214","One body parameter",[723,805,808],{"x":801,"y":806,"style":807},"234","text-anchor:middle;fill:currentColor;font:400 11.5px sans-serif","payload IS the model",[723,810,813],{"x":801,"y":811,"style":812},"252","text-anchor:middle;fill:currentColor;font:400 11px monospace","{\"code\": \"X\", \"percent\": 25}",[714,815],{"x":816,"y":796,"width":785,"height":797,"rx":720,"style":798},"370",[723,818,820],{"x":819,"y":802,"style":727},"535","Two or more",[723,822,823],{"x":819,"y":806,"style":807},"each embedded under its name",[723,825,826],{"x":819,"y":811,"style":812},"{\"campaign\": {...}, \"discount\": {...}}",[828,829,830],"figcaption",{},"The three markers read from three places. The body's shape flips the moment a second body parameter is declared.",[832,833,835],"h2",{"id":834},"the-problem-this-solves","The Problem This Solves",[590,837,838,839,842,843,846,847,850,851,854],{},"You have an endpoint that pages through products. A client sends ",[603,840,841],{},"page=0",", and your ORM builds a query with ",[603,844,845],{},"OFFSET -20",". Another sends ",[603,848,849],{},"per_page=100000"," and your service spends ninety seconds serialising. A third sends ",[603,852,853],{},"sort=created",", which your code silently maps to nothing. None of these are database problems or business-logic problems; they are all \"the interface accepted a value it should have rejected\".",[590,856,857,858,861],{},"Declaring the constraint at the boundary fixes every one of them in a place that also documents itself in OpenAPI. The alternative — an ",[603,859,860],{},"if"," at the top of each handler — is invisible to clients, untested by default, and duplicated across every endpoint that pages.",[832,863,865],{"id":864},"why-the-constraints-behave-the-way-they-do","Why the Constraints Behave the Way They Do",[590,867,868,606,870,606,872,606,874,606,877,610,880,883,884,887,888,890,891,894,895,898,899,606,901,606,903,606,905,606,907,910,911,606,913,606,915,917,918,921],{},[603,869,605],{},[603,871,609],{},[603,873,613],{},[603,875,876],{},"Header",[603,878,879],{},"Cookie",[603,881,882],{},"Form"," are all thin subclasses of the same FastAPI ",[603,885,886],{},"Param","\u002F",[603,889,613],{}," machinery, and every one of them forwards its constraint keywords into a Pydantic ",[603,892,893],{},"FieldInfo",". That is why the vocabulary is identical to ",[603,896,897],{},"Field",": ",[603,900,617],{},[603,902,620],{},[603,904,623],{},[603,906,626],{},[603,908,909],{},"multiple_of"," for numbers; ",[603,912,629],{},[603,914,632],{},[603,916,635],{}," for strings and sequences. There is no separate \"FastAPI validator\" — pydantic-core does the work, which is why the error ",[603,919,920],{},"type"," values on a rejected query parameter are the same ones you see inside a model.",[590,923,924,925,928,929,932,933,935],{},"The consequence: ",[593,926,927],{},"the constraint you can express on a model field, you can express on a parameter",", including ",[603,930,931],{},"Annotated","-attached custom validators. And the error you get back has the same five-key shape, differing only in the ",[603,934,646],{}," prefix.",[832,937,939],{"id":938},"path-parameters","Path Parameters",[590,941,942],{},"Path parameters are always required and always strings on the wire. Constraints are about the coerced value:",[944,945,950],"pre",{"className":946,"code":947,"language":948,"meta":949,"style":949},"language-python shiki shiki-themes github-light-high-contrast","@app.get(\"\u002Freports\u002F{year}\u002F{slug}\")\nasync def get_report(\n    year: Annotated[int, Path(ge=2000, le=2100)],\n    slug: Annotated[str, Path(pattern=r\"^[a-z0-9-]+$\")],\n) -> dict[str, Any]:\n    return {\"year\": year, \"slug\": slug}\n","python","",[603,951,952,983,998,1032,1071,1082],{"__ignoreMap":949},[953,954,956,960,964,968,972,974,977,980],"span",{"class":773,"line":955},1,[953,957,959],{"class":958},"s3dhs","@app.get",[953,961,963],{"class":962},"sigWx","(",[953,965,967],{"class":966},"sYEJz","\"\u002Freports\u002F",[953,969,971],{"class":970},"sTJeM","{year}",[953,973,887],{"class":966},[953,975,976],{"class":970},"{slug}",[953,978,979],{"class":966},"\"",[953,981,982],{"class":962},")\n",[953,984,986,989,992,995],{"class":773,"line":985},2,[953,987,988],{"class":970},"async",[953,990,991],{"class":970}," def",[953,993,994],{"class":958}," get_report",[953,996,997],{"class":962},"(\n",[953,999,1001,1004,1008,1011,1014,1017,1020,1022,1024,1026,1029],{"class":773,"line":1000},3,[953,1002,1003],{"class":962},"    year: Annotated[",[953,1005,1007],{"class":1006},"sacAq","int",[953,1009,1010],{"class":962},", Path(",[953,1012,620],{"class":1013},"sV4o_",[953,1015,1016],{"class":970},"=",[953,1018,1019],{"class":1006},"2000",[953,1021,606],{"class":962},[953,1023,626],{"class":1013},[953,1025,1016],{"class":970},[953,1027,1028],{"class":1006},"2100",[953,1030,1031],{"class":962},")],\n",[953,1033,1035,1038,1041,1043,1045,1047,1050,1052,1055,1058,1061,1064,1067,1069],{"class":773,"line":1034},4,[953,1036,1037],{"class":962},"    slug: Annotated[",[953,1039,1040],{"class":1006},"str",[953,1042,1010],{"class":962},[953,1044,635],{"class":1013},[953,1046,1016],{"class":970},[953,1048,1049],{"class":970},"r",[953,1051,979],{"class":966},[953,1053,1054],{"class":1006},"^[",[953,1056,1057],{"class":970},"a-z0-9-",[953,1059,1060],{"class":1006},"]",[953,1062,1063],{"class":970},"+",[953,1065,1066],{"class":1006},"$",[953,1068,979],{"class":966},[953,1070,1031],{"class":962},[953,1072,1074,1077,1079],{"class":773,"line":1073},5,[953,1075,1076],{"class":962},") -> dict[",[953,1078,1040],{"class":1006},[953,1080,1081],{"class":962},", Any]:\n",[953,1083,1085,1088,1091,1094,1097,1100],{"class":773,"line":1084},6,[953,1086,1087],{"class":970},"    return",[953,1089,1090],{"class":962}," {",[953,1092,1093],{"class":966},"\"year\"",[953,1095,1096],{"class":962},": year, ",[953,1098,1099],{"class":966},"\"slug\"",[953,1101,1102],{"class":962},": slug}\n",[590,1104,1105],{},"Real output:",[944,1107,1111],{"className":1108,"code":1110,"language":723,"meta":949},[1109],"language-text","$ GET \u002Freports\u002F2026\u002Fq2-revenue\n200 OK\n{\n  \"year\": 2026,\n  \"slug\": \"q2-revenue\"\n}\n\n$ GET \u002Freports\u002F1999\u002FQ2_Revenue\n422 Unprocessable Entity\n{\n  \"detail\": [\n    {\n      \"type\": \"greater_than_equal\",\n      \"loc\": [\n        \"path\",\n        \"year\"\n      ],\n      \"msg\": \"Input should be greater than or equal to 2000\",\n      \"input\": \"1999\",\n      \"ctx\": {\n        \"ge\": 2000\n      }\n    },\n    {\n      \"type\": \"string_pattern_mismatch\",\n      \"loc\": [\n        \"path\",\n        \"slug\"\n      ],\n      \"msg\": \"String should match pattern '^[a-z0-9-]+$'\",\n      \"input\": \"Q2_Revenue\",\n      \"ctx\": {\n        \"pattern\": \"^[a-z0-9-]+$\"\n      }\n    }\n  ]\n}\n",[603,1112,1110],{"__ignoreMap":949},[590,1114,1115,1116,1119,1120,1123,1124,1127],{},"Notice ",[603,1117,1118],{},"\"input\": \"1999\""," — a string, not the number 1999. The constraint failed ",[657,1121,1122],{},"after"," coercion, but ",[603,1125,1126],{},"input"," reports the raw value as it arrived. That is genuinely useful in logs: you can see exactly what the client put on the wire.",[590,1129,1130],{},"A slug pattern like this one is worth the two seconds it takes to write. It closes off path traversal attempts, stops case-variant duplicates from reaching your cache keys, and turns \"we got a weird 500 from the CDN\" into a clean 422.",[832,1132,1134],{"id":1133},"query-parameters","Query Parameters",[590,1136,1137],{},"The everyday case is pagination and filtering:",[944,1139,1141],{"className":946,"code":1140,"language":948,"meta":949,"style":949},"@app.get(\"\u002Fproducts\u002F\")\nasync def list_products(\n    q: Annotated[str | None, Query(min_length=2, max_length=40)] = None,\n    page: Annotated[int, Query(ge=1)] = 1,\n    per_page: Annotated[int, Query(ge=1, le=100)] = 20,\n    sort: Annotated[Literal[\"price\", \"name\", \"-price\"], Query()] = \"name\",\n) -> dict[str, Any]:\n    return {\"q\": q, \"page\": page, \"per_page\": per_page, \"sort\": sort}\n",[603,1142,1143,1154,1165,1206,1231,1264,1292,1301],{"__ignoreMap":949},[953,1144,1145,1147,1149,1152],{"class":773,"line":955},[953,1146,959],{"class":958},[953,1148,963],{"class":962},[953,1150,1151],{"class":966},"\"\u002Fproducts\u002F\"",[953,1153,982],{"class":962},[953,1155,1156,1158,1160,1163],{"class":773,"line":985},[953,1157,988],{"class":970},[953,1159,991],{"class":970},[953,1161,1162],{"class":958}," list_products",[953,1164,997],{"class":962},[953,1166,1167,1170,1172,1175,1178,1181,1183,1185,1188,1190,1192,1194,1196,1199,1201,1203],{"class":773,"line":1000},[953,1168,1169],{"class":962},"    q: Annotated[",[953,1171,1040],{"class":1006},[953,1173,1174],{"class":970}," |",[953,1176,1177],{"class":1006}," None",[953,1179,1180],{"class":962},", Query(",[953,1182,629],{"class":1013},[953,1184,1016],{"class":970},[953,1186,1187],{"class":1006},"2",[953,1189,606],{"class":962},[953,1191,632],{"class":1013},[953,1193,1016],{"class":970},[953,1195,758],{"class":1006},[953,1197,1198],{"class":962},")] ",[953,1200,1016],{"class":970},[953,1202,1177],{"class":1006},[953,1204,1205],{"class":962},",\n",[953,1207,1208,1211,1213,1215,1217,1219,1222,1224,1226,1229],{"class":773,"line":1034},[953,1209,1210],{"class":962},"    page: Annotated[",[953,1212,1007],{"class":1006},[953,1214,1180],{"class":962},[953,1216,620],{"class":1013},[953,1218,1016],{"class":970},[953,1220,1221],{"class":1006},"1",[953,1223,1198],{"class":962},[953,1225,1016],{"class":970},[953,1227,1228],{"class":1006}," 1",[953,1230,1205],{"class":962},[953,1232,1233,1236,1238,1240,1242,1244,1246,1248,1250,1252,1255,1257,1259,1262],{"class":773,"line":1073},[953,1234,1235],{"class":962},"    per_page: Annotated[",[953,1237,1007],{"class":1006},[953,1239,1180],{"class":962},[953,1241,620],{"class":1013},[953,1243,1016],{"class":970},[953,1245,1221],{"class":1006},[953,1247,606],{"class":962},[953,1249,626],{"class":1013},[953,1251,1016],{"class":970},[953,1253,1254],{"class":1006},"100",[953,1256,1198],{"class":962},[953,1258,1016],{"class":970},[953,1260,1261],{"class":1006}," 20",[953,1263,1205],{"class":962},[953,1265,1266,1269,1272,1274,1277,1279,1282,1285,1287,1290],{"class":773,"line":1084},[953,1267,1268],{"class":962},"    sort: Annotated[Literal[",[953,1270,1271],{"class":966},"\"price\"",[953,1273,606],{"class":962},[953,1275,1276],{"class":966},"\"name\"",[953,1278,606],{"class":962},[953,1280,1281],{"class":966},"\"-price\"",[953,1283,1284],{"class":962},"], Query()] ",[953,1286,1016],{"class":970},[953,1288,1289],{"class":966}," \"name\"",[953,1291,1205],{"class":962},[953,1293,1295,1297,1299],{"class":773,"line":1294},7,[953,1296,1076],{"class":962},[953,1298,1040],{"class":1006},[953,1300,1081],{"class":962},[953,1302,1304,1306,1308,1311,1314,1317,1320,1323,1326,1329],{"class":773,"line":1303},8,[953,1305,1087],{"class":970},[953,1307,1090],{"class":962},[953,1309,1310],{"class":966},"\"q\"",[953,1312,1313],{"class":962},": q, ",[953,1315,1316],{"class":966},"\"page\"",[953,1318,1319],{"class":962},": page, ",[953,1321,1322],{"class":966},"\"per_page\"",[953,1324,1325],{"class":962},": per_page, ",[953,1327,1328],{"class":966},"\"sort\"",[953,1330,1331],{"class":962},": sort}\n",[590,1333,1334],{},"One request violating all four:",[944,1336,1339],{"className":1337,"code":1338,"language":723,"meta":949},[1109],"$ GET \u002Fproducts\u002F?q=a&page=0&per_page=500&sort=created\n422 Unprocessable Entity\n{\n  \"detail\": [\n    {\n      \"type\": \"string_too_short\",\n      \"loc\": [\n        \"query\",\n        \"q\"\n      ],\n      \"msg\": \"String should have at least 2 characters\",\n      \"input\": \"a\",\n      \"ctx\": {\n        \"min_length\": 2\n      }\n    },\n    {\n      \"type\": \"greater_than_equal\",\n      \"loc\": [\n        \"query\",\n        \"page\"\n      ],\n      \"msg\": \"Input should be greater than or equal to 1\",\n      \"input\": \"0\",\n      \"ctx\": {\n        \"ge\": 1\n      }\n    },\n    {\n      \"type\": \"less_than_equal\",\n      \"loc\": [\n        \"query\",\n        \"per_page\"\n      ],\n      \"msg\": \"Input should be less than or equal to 100\",\n      \"input\": \"500\",\n      \"ctx\": {\n        \"le\": 100\n      }\n    },\n    {\n      \"type\": \"literal_error\",\n      \"loc\": [\n        \"query\",\n        \"sort\"\n      ],\n      \"msg\": \"Input should be 'price', 'name' or '-price'\",\n      \"input\": \"created\",\n      \"ctx\": {\n        \"expected\": \"'price', 'name' or '-price'\"\n      }\n    }\n  ]\n}\n",[603,1340,1338],{"__ignoreMap":949},[590,1342,1343,1345,1346,1349,1350,1353],{},[603,1344,665],{}," deserves a moment. It costs one import, it rejects unknown sort keys before they reach your ORM, it renders as an enum dropdown in Swagger UI, and its ",[603,1347,1348],{},"ctx.expected"," hands the client the valid set. A hand-rolled ",[603,1351,1352],{},"if sort not in {...}: raise HTTPException(400)"," gives you none of that and does not appear in the schema at all.",[590,1355,1356,1357,1360,1361,636],{},"And the ",[603,1358,1359],{},"per_page"," ceiling is not pedantry: an unbounded page size is the single most reliable way for one client to exhaust your connection pool. That failure mode and its blast radius are covered in ",[675,1362,1364],{"href":1363},"\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ffixing-asyncpg-pool-exhaustion\u002F","fixing asyncpg pool exhaustion",[1366,1367,1369],"h3",{"id":1368},"repeated-keys-as-lists","Repeated keys as lists",[944,1371,1373],{"className":946,"code":1372,"language":948,"meta":949,"style":949},"@app.get(\"\u002Fproducts\u002Fby-tag\")\nasync def by_tag(\n    tag: Annotated[list[str], Query(min_length=1)] = [],\n    ids: Annotated[list[int] | None, Query(alias=\"id\")] = None,\n) -> dict[str, Any]:\n    # A repeated query key collects into a list; alias renames the wire key.\n    return {\"tag\": tag, \"ids\": ids}\n",[603,1374,1375,1386,1397,1420,1452,1460,1466],{"__ignoreMap":949},[953,1376,1377,1379,1381,1384],{"class":773,"line":955},[953,1378,959],{"class":958},[953,1380,963],{"class":962},[953,1382,1383],{"class":966},"\"\u002Fproducts\u002Fby-tag\"",[953,1385,982],{"class":962},[953,1387,1388,1390,1392,1395],{"class":773,"line":985},[953,1389,988],{"class":970},[953,1391,991],{"class":970},[953,1393,1394],{"class":958}," by_tag",[953,1396,997],{"class":962},[953,1398,1399,1402,1404,1407,1409,1411,1413,1415,1417],{"class":773,"line":1000},[953,1400,1401],{"class":962},"    tag: Annotated[list[",[953,1403,1040],{"class":1006},[953,1405,1406],{"class":962},"], Query(",[953,1408,629],{"class":1013},[953,1410,1016],{"class":970},[953,1412,1221],{"class":1006},[953,1414,1198],{"class":962},[953,1416,1016],{"class":970},[953,1418,1419],{"class":962}," [],\n",[953,1421,1422,1425,1427,1430,1433,1435,1437,1439,1441,1444,1446,1448,1450],{"class":773,"line":1034},[953,1423,1424],{"class":962},"    ids: Annotated[list[",[953,1426,1007],{"class":1006},[953,1428,1429],{"class":962},"] ",[953,1431,1432],{"class":970},"|",[953,1434,1177],{"class":1006},[953,1436,1180],{"class":962},[953,1438,651],{"class":1013},[953,1440,1016],{"class":970},[953,1442,1443],{"class":966},"\"id\"",[953,1445,1198],{"class":962},[953,1447,1016],{"class":970},[953,1449,1177],{"class":1006},[953,1451,1205],{"class":962},[953,1453,1454,1456,1458],{"class":773,"line":1073},[953,1455,1076],{"class":962},[953,1457,1040],{"class":1006},[953,1459,1081],{"class":962},[953,1461,1462],{"class":773,"line":1084},[953,1463,1465],{"class":1464},"sFeEa","    # A repeated query key collects into a list; alias renames the wire key.\n",[953,1467,1468,1470,1472,1475,1478,1481],{"class":773,"line":1294},[953,1469,1087],{"class":970},[953,1471,1090],{"class":962},[953,1473,1474],{"class":966},"\"tag\"",[953,1476,1477],{"class":962},": tag, ",[953,1479,1480],{"class":966},"\"ids\"",[953,1482,1483],{"class":962},": ids}\n",[944,1485,1488],{"className":1486,"code":1487,"language":723,"meta":949},[1109],"$ GET \u002Fproducts\u002Fby-tag?tag=sale&tag=new&id=1&id=2\n200 OK\n{\n  \"tag\": [\n    \"sale\",\n    \"new\"\n  ],\n  \"ids\": [\n    1,\n    2\n  ]\n}\n\n$ GET \u002Fproducts\u002Fby-tag?tag=&id=abc\n422 Unprocessable Entity\n{\n  \"detail\": [\n    {\n      \"type\": \"int_parsing\",\n      \"loc\": [\n        \"query\",\n        \"id\",\n        0\n      ],\n      \"msg\": \"Input should be a valid integer, unable to parse string as an integer\",\n      \"input\": \"abc\"\n    }\n  ]\n}\n",[603,1489,1487],{"__ignoreMap":949},[590,1491,1492,1493,898,1495,1498,1499,1502,1503,1506,1507,1510,1511,1514,1515,636],{},"Two things to read off that transcript. First, the element index appears in ",[603,1494,646],{},[603,1496,1497],{},"[\"query\", \"id\", 0]",". A client rendering error messages next to individual chips in a tag input has everything it needs. Second, ",[603,1500,1501],{},"min_length=1"," on a ",[603,1504,1505],{},"list[str]"," constrains the ",[593,1508,1509],{},"list",", not its elements — ",[603,1512,1513],{},"tag="," produced a one-element list containing the empty string, which satisfies \"at least one item\". To constrain the elements, put the constraint on the element type: ",[603,1516,1517],{},"list[Annotated[str, Field(min_length=1)]]",[590,1519,1520,1521,1524,1525,1528,1529,1532],{},"The mutable default ",[603,1522,1523],{},"= []"," is safe here in a way it is not in ordinary Python, because FastAPI never mutates it; it is read as the field default on each request. It still reads badly, and ",[603,1526,1527],{},"= None"," with a ",[603,1530,1531],{},"list[str] | None"," annotation is the clearer habit.",[1366,1534,1536],{"id":1535},"aliases-for-wire-names-you-do-not-control","Aliases for wire names you do not control",[944,1538,1540],{"className":946,"code":1539,"language":948,"meta":949,"style":949},"@app.get(\"\u002Flegacy\u002F\")\nasync def legacy(\n    customer_id: Annotated[int, Query(alias=\"customerId\")],\n    include: Annotated[str | None, Query(alias=\"include[]\")] = None,\n) -> dict[str, Any]:\n    return {\"customer_id\": customer_id, \"include\": include}\n",[603,1541,1542,1553,1564,1582,1610,1618],{"__ignoreMap":949},[953,1543,1544,1546,1548,1551],{"class":773,"line":955},[953,1545,959],{"class":958},[953,1547,963],{"class":962},[953,1549,1550],{"class":966},"\"\u002Flegacy\u002F\"",[953,1552,982],{"class":962},[953,1554,1555,1557,1559,1562],{"class":773,"line":985},[953,1556,988],{"class":970},[953,1558,991],{"class":970},[953,1560,1561],{"class":958}," legacy",[953,1563,997],{"class":962},[953,1565,1566,1569,1571,1573,1575,1577,1580],{"class":773,"line":1000},[953,1567,1568],{"class":962},"    customer_id: Annotated[",[953,1570,1007],{"class":1006},[953,1572,1180],{"class":962},[953,1574,651],{"class":1013},[953,1576,1016],{"class":970},[953,1578,1579],{"class":966},"\"customerId\"",[953,1581,1031],{"class":962},[953,1583,1584,1587,1589,1591,1593,1595,1597,1599,1602,1604,1606,1608],{"class":773,"line":1034},[953,1585,1586],{"class":962},"    include: Annotated[",[953,1588,1040],{"class":1006},[953,1590,1174],{"class":970},[953,1592,1177],{"class":1006},[953,1594,1180],{"class":962},[953,1596,651],{"class":1013},[953,1598,1016],{"class":970},[953,1600,1601],{"class":966},"\"include[]\"",[953,1603,1198],{"class":962},[953,1605,1016],{"class":970},[953,1607,1177],{"class":1006},[953,1609,1205],{"class":962},[953,1611,1612,1614,1616],{"class":773,"line":1073},[953,1613,1076],{"class":962},[953,1615,1040],{"class":1006},[953,1617,1081],{"class":962},[953,1619,1620,1622,1624,1627,1630,1633],{"class":773,"line":1084},[953,1621,1087],{"class":970},[953,1623,1090],{"class":962},[953,1625,1626],{"class":966},"\"customer_id\"",[953,1628,1629],{"class":962},": customer_id, ",[953,1631,1632],{"class":966},"\"include\"",[953,1634,1635],{"class":962},": include}\n",[944,1637,1640],{"className":1638,"code":1639,"language":723,"meta":949},[1109],"$ GET \u002Flegacy\u002F?customerId=88&include[]=orders\n200 OK\n{\n  \"customer_id\": 88,\n  \"include\": \"orders\"\n}\n\n$ GET \u002Flegacy\u002F?customer_id=88\n422 Unprocessable Entity\n{\n  \"detail\": [\n    {\n      \"type\": \"missing\",\n      \"loc\": [\n        \"query\",\n        \"customerId\"\n      ],\n      \"msg\": \"Field required\",\n      \"input\": null\n    }\n  ]\n}\n",[603,1641,1639],{"__ignoreMap":949},[590,1643,1644,1645,1648,1649,1652,1653,1656],{},"The alias is the contract. ",[603,1646,1647],{},"customer_id"," is now simply not a parameter this endpoint has. That is usually what you want — one wire name, one meaning — but it makes aliasing a breaking change for existing callers. During a migration, accept both by using Pydantic's ",[603,1650,1651],{},"AliasChoices"," in a ",[603,1654,1655],{},"Query(validation_alias=AliasChoices(\"customerId\", \"customer_id\"))",", ship it, then remove the old name once your client metrics show it unused.",[590,1658,1659,1662],{},[603,1660,1661],{},"include[]"," shows the other reason aliases exist: bracket-suffixed keys from PHP-era clients and some HTTP libraries are not valid Python identifiers. The alias is the only way to accept them.",[832,1664,1666],{"id":1665},"body-parameters","Body Parameters",[590,1668,1669,1670,1672],{},"With one body parameter, the JSON payload ",[657,1671,659],{}," the model:",[944,1674,1676],{"className":946,"code":1675,"language":948,"meta":949,"style":949},"class Discount(BaseModel):\n    code: str = Field(min_length=4, max_length=16, pattern=r\"^[A-Z0-9]+$\")\n    percent: int = Field(gt=0, le=90)\n\n\n@app.post(\"\u002Fdiscounts\u002F\")\nasync def create_discount(discount: Discount) -> dict[str, Any]:\n    return discount.model_dump()\n",[603,1677,1678,1694,1748,1777,1783,1787,1799,1815],{"__ignoreMap":949},[953,1679,1680,1683,1686,1688,1691],{"class":773,"line":955},[953,1681,1682],{"class":970},"class",[953,1684,1685],{"class":1013}," Discount",[953,1687,963],{"class":962},[953,1689,1690],{"class":1006},"BaseModel",[953,1692,1693],{"class":962},"):\n",[953,1695,1696,1699,1701,1704,1707,1709,1711,1714,1716,1718,1720,1723,1725,1727,1729,1731,1733,1735,1738,1740,1742,1744,1746],{"class":773,"line":985},[953,1697,1698],{"class":962},"    code: ",[953,1700,1040],{"class":1006},[953,1702,1703],{"class":970}," =",[953,1705,1706],{"class":962}," Field(",[953,1708,629],{"class":1013},[953,1710,1016],{"class":970},[953,1712,1713],{"class":1006},"4",[953,1715,606],{"class":962},[953,1717,632],{"class":1013},[953,1719,1016],{"class":970},[953,1721,1722],{"class":1006},"16",[953,1724,606],{"class":962},[953,1726,635],{"class":1013},[953,1728,1016],{"class":970},[953,1730,1049],{"class":970},[953,1732,979],{"class":966},[953,1734,1054],{"class":1006},[953,1736,1737],{"class":970},"A-Z0-9",[953,1739,1060],{"class":1006},[953,1741,1063],{"class":970},[953,1743,1066],{"class":1006},[953,1745,979],{"class":966},[953,1747,982],{"class":962},[953,1749,1750,1753,1755,1757,1759,1761,1763,1766,1768,1770,1772,1775],{"class":773,"line":1000},[953,1751,1752],{"class":962},"    percent: ",[953,1754,1007],{"class":1006},[953,1756,1703],{"class":970},[953,1758,1706],{"class":962},[953,1760,617],{"class":1013},[953,1762,1016],{"class":970},[953,1764,1765],{"class":1006},"0",[953,1767,606],{"class":962},[953,1769,626],{"class":1013},[953,1771,1016],{"class":970},[953,1773,1774],{"class":1006},"90",[953,1776,982],{"class":962},[953,1778,1779],{"class":773,"line":1034},[953,1780,1782],{"emptyLinePlaceholder":1781},true,"\n",[953,1784,1785],{"class":773,"line":1073},[953,1786,1782],{"emptyLinePlaceholder":1781},[953,1788,1789,1792,1794,1797],{"class":773,"line":1084},[953,1790,1791],{"class":958},"@app.post",[953,1793,963],{"class":962},[953,1795,1796],{"class":966},"\"\u002Fdiscounts\u002F\"",[953,1798,982],{"class":962},[953,1800,1801,1803,1805,1808,1811,1813],{"class":773,"line":1294},[953,1802,988],{"class":970},[953,1804,991],{"class":970},[953,1806,1807],{"class":958}," create_discount",[953,1809,1810],{"class":962},"(discount: Discount) -> dict[",[953,1812,1040],{"class":1006},[953,1814,1081],{"class":962},[953,1816,1817,1819],{"class":773,"line":1303},[953,1818,1087],{"class":970},[953,1820,1821],{"class":962}," discount.model_dump()\n",[944,1823,1826],{"className":1824,"code":1825,"language":723,"meta":949},[1109],"$ POST \u002Fdiscounts\u002F  {\"code\": \"SUMMER26\", \"percent\": 25}\n200 OK\n{\n  \"code\": \"SUMMER26\",\n  \"percent\": 25\n}\n\n$ POST \u002Fdiscounts\u002F  {\"code\": \"sum\", \"percent\": 95}\n422 Unprocessable Entity\n{\n  \"detail\": [\n    {\n      \"type\": \"string_too_short\",\n      \"loc\": [\n        \"body\",\n        \"code\"\n      ],\n      \"msg\": \"String should have at least 4 characters\",\n      \"input\": \"sum\",\n      \"ctx\": {\n        \"min_length\": 4\n      }\n    },\n    {\n      \"type\": \"less_than_equal\",\n      \"loc\": [\n        \"body\",\n        \"percent\"\n      ],\n      \"msg\": \"Input should be less than or equal to 90\",\n      \"input\": 95,\n      \"ctx\": {\n        \"le\": 90\n      }\n    }\n  ]\n}\n",[603,1827,1825],{"__ignoreMap":949},[590,1829,1830,1831,1833,1834,1837,1838,1841,1842,1844],{},"Here ",[603,1832,1126],{}," is the integer ",[603,1835,1836],{},"95",", not the string ",[603,1839,1840],{},"\"95\"",", because JSON carries types. Compare with the query-string transcripts above, where every ",[603,1843,1126],{}," is a string. If you build alerting on validation failures, that difference matters for your log schema.",[1366,1846,1848],{"id":1847},"the-single-model-versus-multiple-parameters-rule","The single-model versus multiple-parameters rule",[590,1850,1851],{},"This is the rule that breaks clients in production, so it is worth stating precisely:",[597,1853,1854,1860,1873],{},[600,1855,1856,1859],{},[593,1857,1858],{},"Exactly one body parameter, and it is a model:"," the request body is that model's JSON object directly.",[600,1861,1862,1869,1870,636],{},[593,1863,1864,1865,1868],{},"Exactly one body parameter with ",[603,1866,1867],{},"Body(embed=True)",":"," the body becomes ",[603,1871,1872],{},"{\"\u003Cparam_name>\": {…}}",[600,1874,1875,1882,1883,1886],{},[593,1876,1877,1878,1881],{},"Two or more body parameters (models or ",[603,1879,1880],{},"Body()"," scalars):"," FastAPI embeds ",[657,1884,1885],{},"every"," one under its parameter name automatically. There is no way to keep one of them flat.",[944,1888,1890],{"className":946,"code":1889,"language":948,"meta":949,"style":949},"@app.post(\"\u002Fcampaigns\u002F\")\nasync def create_campaign(\n    campaign: Campaign,\n    discount: Discount,\n    priority: Annotated[int, Body(ge=0, le=9)] = 0,\n) -> dict[str, Any]:\n    # Multiple body parameters -> every one is embedded under its parameter name.\n    return {\"campaign\": campaign.model_dump(), \"discount\": discount.model_dump(), \"priority\": priority}\n",[603,1891,1892,1903,1914,1919,1924,1958,1966,1971],{"__ignoreMap":949},[953,1893,1894,1896,1898,1901],{"class":773,"line":955},[953,1895,1791],{"class":958},[953,1897,963],{"class":962},[953,1899,1900],{"class":966},"\"\u002Fcampaigns\u002F\"",[953,1902,982],{"class":962},[953,1904,1905,1907,1909,1912],{"class":773,"line":985},[953,1906,988],{"class":970},[953,1908,991],{"class":970},[953,1910,1911],{"class":958}," create_campaign",[953,1913,997],{"class":962},[953,1915,1916],{"class":773,"line":1000},[953,1917,1918],{"class":962},"    campaign: Campaign,\n",[953,1920,1921],{"class":773,"line":1034},[953,1922,1923],{"class":962},"    discount: Discount,\n",[953,1925,1926,1929,1931,1934,1936,1938,1940,1942,1944,1946,1949,1951,1953,1956],{"class":773,"line":1073},[953,1927,1928],{"class":962},"    priority: Annotated[",[953,1930,1007],{"class":1006},[953,1932,1933],{"class":962},", Body(",[953,1935,620],{"class":1013},[953,1937,1016],{"class":970},[953,1939,1765],{"class":1006},[953,1941,606],{"class":962},[953,1943,626],{"class":1013},[953,1945,1016],{"class":970},[953,1947,1948],{"class":1006},"9",[953,1950,1198],{"class":962},[953,1952,1016],{"class":970},[953,1954,1955],{"class":1006}," 0",[953,1957,1205],{"class":962},[953,1959,1960,1962,1964],{"class":773,"line":1084},[953,1961,1076],{"class":962},[953,1963,1040],{"class":1006},[953,1965,1081],{"class":962},[953,1967,1968],{"class":773,"line":1294},[953,1969,1970],{"class":1464},"    # Multiple body parameters -> every one is embedded under its parameter name.\n",[953,1972,1973,1975,1977,1980,1983,1986,1989,1992],{"class":773,"line":1303},[953,1974,1087],{"class":970},[953,1976,1090],{"class":962},[953,1978,1979],{"class":966},"\"campaign\"",[953,1981,1982],{"class":962},": campaign.model_dump(), ",[953,1984,1985],{"class":966},"\"discount\"",[953,1987,1988],{"class":962},": discount.model_dump(), ",[953,1990,1991],{"class":966},"\"priority\"",[953,1993,1994],{"class":962},": priority}\n",[944,1996,1999],{"className":1997,"code":1998,"language":723,"meta":949},[1109],"$ POST \u002Fcampaigns\u002F  {\"campaign\": {\"name\": \"Summer\"}, \"discount\": {\"code\": \"SUMMER26\", \"percent\": 25}, \"priority\": 3}\n200 OK\n{\n  \"campaign\": {\n    \"name\": \"Summer\"\n  },\n  \"discount\": {\n    \"code\": \"SUMMER26\",\n    \"percent\": 25\n  },\n  \"priority\": 3\n}\n\n$ POST \u002Fcampaigns\u002F  {\"name\": \"Summer\", \"code\": \"SUMMER26\", \"percent\": 25}\n422 Unprocessable Entity\n{\n  \"detail\": [\n    {\n      \"type\": \"missing\",\n      \"loc\": [\n        \"body\",\n        \"campaign\"\n      ],\n      \"msg\": \"Field required\",\n      \"input\": null\n    },\n    {\n      \"type\": \"missing\",\n      \"loc\": [\n        \"body\",\n        \"discount\"\n      ],\n      \"msg\": \"Field required\",\n      \"input\": null\n    }\n  ]\n}\n",[603,2000,1998],{"__ignoreMap":949},[590,2002,2003,2004,2007,2008,2011,2012,2015,2016,2019],{},"The second transcript is the shape of the outage. A deployed endpoint took a flat ",[603,2005,2006],{},"Discount","; someone added a ",[603,2009,2010],{},"campaign: Campaign"," parameter; every existing client immediately started getting ",[603,2013,2014],{},"missing"," errors on ",[603,2017,2018],{},"discount"," — a name that appears nowhere in their code, because it is your Python parameter name.",[590,2021,2022,2025,2026,2029,2030,2034],{},[593,2023,2024],{},"The defensive habit:"," if an endpoint might ever grow a second body input, declare a wrapper model with both as fields from day one. ",[603,2027,2028],{},"CreateCampaignRequest(campaign=…, discount=…)"," gives you the same JSON shape as auto-embedding, but the shape is explicit in your code, versioned with your schema, and adding a third field is an additive change rather than a structural flip. Where the endpoints themselves need to change shape, ",[675,2031,2033],{"href":2032},"\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization\u002Fversioning-apis-with-routers\u002F","versioning APIs with routers"," is the escape hatch.",[832,2036,2038],{"id":2037},"verification","Verification",[590,2040,2041],{},"The fastest confirmation is the generated schema — the constraints are in it, so a client generator will enforce them too:",[944,2043,2045],{"className":946,"code":2044,"language":948,"meta":949,"style":949},"def test_openapi_carries_constraints():\n    schema = app.openapi()\n    params = schema[\"paths\"][\"\u002Fproducts\u002F\"][\"get\"][\"parameters\"]\n    per_page = next(p for p in params if p[\"name\"] == \"per_page\")\n    assert per_page[\"schema\"][\"maximum\"] == 100\n    assert per_page[\"schema\"][\"minimum\"] == 1\n",[603,2046,2047,2058,2068,2099,2141,2164],{"__ignoreMap":949},[953,2048,2049,2052,2055],{"class":773,"line":955},[953,2050,2051],{"class":970},"def",[953,2053,2054],{"class":958}," test_openapi_carries_constraints",[953,2056,2057],{"class":962},"():\n",[953,2059,2060,2063,2065],{"class":773,"line":985},[953,2061,2062],{"class":962},"    schema ",[953,2064,1016],{"class":970},[953,2066,2067],{"class":962}," app.openapi()\n",[953,2069,2070,2073,2075,2078,2081,2084,2086,2088,2091,2093,2096],{"class":773,"line":1000},[953,2071,2072],{"class":962},"    params ",[953,2074,1016],{"class":970},[953,2076,2077],{"class":962}," schema[",[953,2079,2080],{"class":966},"\"paths\"",[953,2082,2083],{"class":962},"][",[953,2085,1151],{"class":966},[953,2087,2083],{"class":962},[953,2089,2090],{"class":966},"\"get\"",[953,2092,2083],{"class":962},[953,2094,2095],{"class":966},"\"parameters\"",[953,2097,2098],{"class":962},"]\n",[953,2100,2101,2104,2106,2109,2112,2115,2118,2121,2124,2126,2129,2131,2133,2136,2139],{"class":773,"line":1034},[953,2102,2103],{"class":962},"    per_page ",[953,2105,1016],{"class":970},[953,2107,2108],{"class":1006}," next",[953,2110,2111],{"class":962},"(p ",[953,2113,2114],{"class":970},"for",[953,2116,2117],{"class":962}," p ",[953,2119,2120],{"class":970},"in",[953,2122,2123],{"class":962}," params ",[953,2125,860],{"class":970},[953,2127,2128],{"class":962}," p[",[953,2130,1276],{"class":966},[953,2132,1429],{"class":962},[953,2134,2135],{"class":970},"==",[953,2137,2138],{"class":966}," \"per_page\"",[953,2140,982],{"class":962},[953,2142,2143,2146,2149,2152,2154,2157,2159,2161],{"class":773,"line":1073},[953,2144,2145],{"class":970},"    assert",[953,2147,2148],{"class":962}," per_page[",[953,2150,2151],{"class":966},"\"schema\"",[953,2153,2083],{"class":962},[953,2155,2156],{"class":966},"\"maximum\"",[953,2158,1429],{"class":962},[953,2160,2135],{"class":970},[953,2162,2163],{"class":1006}," 100\n",[953,2165,2166,2168,2170,2172,2174,2177,2179,2181],{"class":773,"line":1084},[953,2167,2145],{"class":970},[953,2169,2148],{"class":962},[953,2171,2151],{"class":966},[953,2173,2083],{"class":962},[953,2175,2176],{"class":966},"\"minimum\"",[953,2178,1429],{"class":962},[953,2180,2135],{"class":970},[953,2182,2183],{"class":1006}," 1\n",[590,2185,2186],{},"For the body-shape rule, assert on the shape rather than on prose:",[944,2188,2190],{"className":946,"code":2189,"language":948,"meta":949,"style":949},"def test_multiple_body_params_are_embedded():\n    body = app.openapi()[\"paths\"][\"\u002Fcampaigns\u002F\"][\"post\"][\"requestBody\"]\n    ref = body[\"content\"][\"application\u002Fjson\"][\"schema\"][\"$ref\"]\n    assert ref.endswith(\"Body_create_campaign_campaigns__post\")\n",[603,2191,2192,2201,2229,2258],{"__ignoreMap":949},[953,2193,2194,2196,2199],{"class":773,"line":955},[953,2195,2051],{"class":970},[953,2197,2198],{"class":958}," test_multiple_body_params_are_embedded",[953,2200,2057],{"class":962},[953,2202,2203,2206,2208,2211,2213,2215,2217,2219,2222,2224,2227],{"class":773,"line":985},[953,2204,2205],{"class":962},"    body ",[953,2207,1016],{"class":970},[953,2209,2210],{"class":962}," app.openapi()[",[953,2212,2080],{"class":966},[953,2214,2083],{"class":962},[953,2216,1900],{"class":966},[953,2218,2083],{"class":962},[953,2220,2221],{"class":966},"\"post\"",[953,2223,2083],{"class":962},[953,2225,2226],{"class":966},"\"requestBody\"",[953,2228,2098],{"class":962},[953,2230,2231,2234,2236,2239,2242,2244,2247,2249,2251,2253,2256],{"class":773,"line":1000},[953,2232,2233],{"class":962},"    ref ",[953,2235,1016],{"class":970},[953,2237,2238],{"class":962}," body[",[953,2240,2241],{"class":966},"\"content\"",[953,2243,2083],{"class":962},[953,2245,2246],{"class":966},"\"application\u002Fjson\"",[953,2248,2083],{"class":962},[953,2250,2151],{"class":966},[953,2252,2083],{"class":962},[953,2254,2255],{"class":966},"\"$ref\"",[953,2257,2098],{"class":962},[953,2259,2260,2262,2265,2268],{"class":773,"line":1034},[953,2261,2145],{"class":970},[953,2263,2264],{"class":962}," ref.endswith(",[953,2266,2267],{"class":966},"\"Body_create_campaign_campaigns__post\"",[953,2269,982],{"class":962},[590,2271,2272,2273,2276,2277,636],{},"That generated ",[603,2274,2275],{},"Body_…"," model name is FastAPI telling you it built a wrapper. Seeing it in your OpenAPI diff is the earliest possible warning that a body contract just changed shape — worth wiring into CI alongside the practices in ",[675,2278,2280],{"href":2279},"\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fcustomizing-openapi-schema-generation-in-fastapi\u002F","customizing OpenAPI schema generation",[832,2282,2284],{"id":2283},"trade-offs-and-when-not-to","Trade-offs and When Not To",[590,2286,2287,2290,2291,2293,2294,2296],{},[593,2288,2289],{},"Constraints on the parameter versus on the model."," A ",[603,2292,1359],{}," ceiling belongs on ",[603,2295,605],{}," — it is a property of this endpoint's capacity, not of any domain object. A discount code's format belongs on the model, because it should hold anywhere a discount code appears, including in background jobs that never see an HTTP request.",[590,2298,2299,2302,2303,2306,2307,2310,2311,2314,2315,636],{},[593,2300,2301],{},"Patterns are a blunt instrument."," ",[603,2304,2305],{},"pattern=r\"^[a-z0-9-]+$\""," is excellent for slugs and terrible for email addresses or names, where it will reject legitimate international input. Where a regex would need exceptions, use a Pydantic type such as ",[603,2308,2309],{},"EmailStr"," or an ",[603,2312,2313],{},"AfterValidator"," that expresses the actual rule; see ",[675,2316,2318],{"href":2317},"\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fcreating-reusable-custom-validators-in-pydantic\u002F","creating reusable custom validators",[590,2320,2321,2324,2325,2327,2328,2331],{},[593,2322,2323],{},"Very strict input is a compatibility liability."," Adding ",[603,2326,635],{}," to an existing parameter can reject traffic that has worked for a year. Deploy new constraints in report-only mode first: log what ",[657,2329,2330],{},"would"," have been rejected, look at the numbers, then enforce.",[590,2333,2334,2337,2338,2341],{},[593,2335,2336],{},"Do not encode business rules as parameter constraints."," \"Discount may not exceed 90%\" is arguably a business rule that will change by market and by campaign. If it lives in ",[603,2339,2340],{},"Field(le=90)"," it also lives in your public schema, and changing it is an API change. Rules that vary by context belong in the service layer.",[832,2343,2345],{"id":2344},"faq","FAQ",[590,2347,2348,2351,2352,2354,2355,2357],{},[593,2349,2350],{},"Why did adding a second body parameter break my existing clients?","\nWith one body parameter the JSON payload is the model itself. Add a second and FastAPI embeds both under their parameter names, so the payload must become an object keyed by those names. Existing clients then get a 422 with ",[603,2353,2014],{}," errors whose ",[603,2356,646],{}," names your Python parameter, not a model field.",[590,2359,2360,2363,2364,2367,2368,2370,2371,636],{},[593,2361,2362],{},"How do I accept the same query parameter repeated several times?","\nDeclare it as a list, for example ",[603,2365,2366],{},"Annotated[list[str], Query()]",". FastAPI collects every occurrence of the key into the list. Element-level failures report an index in ",[603,2369,646],{},", such as ",[603,2372,2373],{},"query, id, 0",[590,2375,2376,2383,2384,2386,2387,2390,2391,2393],{},[593,2377,2378,2379,2382],{},"Does ",[603,2380,2381],{},"Query(alias=...)"," still accept the original Python name?","\nNo. The alias replaces the wire name entirely, so sending the snake_case name produces a ",[603,2385,2014],{}," error on the aliased name. Use ",[603,2388,2389],{},"validation_alias"," with an ",[603,2392,1651],{}," if you genuinely need to accept both during a migration.",[590,2395,2396,2399],{},[593,2397,2398],{},"Can a path parameter have a default value?","\nNo. The path segment must be present for the route to match at all, so a default is unreachable. A request without the segment gets a 404 from the router, not a 422 from validation.",[590,2401,2402,2408,2409,2411,2412,2414],{},[593,2403,2404,2405,2407],{},"Where should a constraint live — on ",[603,2406,605],{}," or on the Pydantic model?","\nPut it on the model when the rule belongs to the data and should appear wherever that data is used. Put it on ",[603,2410,605],{}," or ",[603,2413,609],{}," when the rule is about this endpoint's interface, such as a pagination ceiling that other callers of the same model do not share.",[832,2416,2418],{"id":2417},"related-reading","Related Reading",[597,2420,2421,2429,2444,2458],{},[600,2422,2423,2302,2426,2428],{},[593,2424,2425],{},"Up to the guide:",[675,2427,678],{"href":677}," for how FastAPI picks a parameter's source in the first place.",[600,2430,2431,2302,2434,2438,2439,2443],{},[593,2432,2433],{},"Sibling pages:",[675,2435,2437],{"href":2436},"\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fvalidating-file-uploads-and-forms\u002F","Validating File Uploads and Forms"," for the multipart equivalent of these rules, and ",[675,2440,2442],{"href":2441},"\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Foptional-vs-nullable-fields\u002F","Optional vs Nullable Fields"," for what a default really means.",[600,2445,2446,2302,2449,2453,2454,2457],{},[593,2447,2448],{},"Reusing declarations:",[675,2450,2452],{"href":2451},"\u002Fadvanced-pydantic-validation-serialization\u002Ftype-hinting-ide-integration\u002Fannotated-dependencies-and-reusable-types\u002F","Annotated Dependencies and Reusable Types"," turns a repeated ",[603,2455,2456],{},"Query(ge=1)"," into a named type.",[600,2459,2460,2302,2463,2467],{},[593,2461,2462],{},"Shaping the errors:",[675,2464,2466],{"href":2465},"\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fcustomising-validation-error-responses\u002F","Customising Validation Error Responses"," if the default 422 envelope does not suit your clients.",[2469,2470,2471],"style",{},"html pre.shiki code .s3dhs, html code.shiki .s3dhs{--shiki-default:#622CBC}html pre.shiki code .sigWx, html code.shiki .sigWx{--shiki-default:#0E1116}html pre.shiki code .sYEJz, html code.shiki .sYEJz{--shiki-default:#032563}html pre.shiki code .sTJeM, html code.shiki .sTJeM{--shiki-default:#A0111F}html pre.shiki code .sacAq, html code.shiki .sacAq{--shiki-default:#023B95}html pre.shiki code .sV4o_, html code.shiki .sV4o_{--shiki-default:#702C00}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sFeEa, html code.shiki .sFeEa{--shiki-default:#66707B}",{"title":949,"searchDepth":985,"depth":985,"links":2473},[2474,2475,2476,2477,2481,2484,2485,2486,2487],{"id":834,"depth":985,"text":835},{"id":864,"depth":985,"text":865},{"id":938,"depth":985,"text":939},{"id":1133,"depth":985,"text":1134,"children":2478},[2479,2480],{"id":1368,"depth":1000,"text":1369},{"id":1535,"depth":1000,"text":1536},{"id":1665,"depth":985,"text":1666,"children":2482},[2483],{"id":1847,"depth":1000,"text":1848},{"id":2037,"depth":985,"text":2038},{"id":2283,"depth":985,"text":2284},{"id":2344,"depth":985,"text":2345},{"id":2417,"depth":985,"text":2418},"2026-07-20","Constrain FastAPI parameters with Query, Path and Body: bounds, patterns, repeated list params, aliases, and the multiple-body-parameter rule, with real 422s.","md",[2492,2494,2496,2499,2501],{"q":2350,"a":2493},"With one body parameter the JSON payload is the model itself. Add a second and FastAPI embeds both under their parameter names, so the payload must become an object keyed by those names. Existing clients then get a 422 with missing errors whose loc names your Python parameter, not a model field.",{"q":2362,"a":2495},"Declare it as a list, for example Annotated[list[str], Query()]. FastAPI collects every occurrence of the key into the list. Element-level failures report an index in loc, such as query, id, 0.",{"q":2497,"a":2498},"Does Query(alias=...) still accept the original Python name?","No. The alias replaces the wire name entirely, so sending the snake_case name produces a missing error on the aliased name. Use validation_alias with an AliasChoices if you genuinely need to accept both during a migration.",{"q":2398,"a":2500},"No. The path segment must be present for the route to match at all, so a default is unreachable. A request without the segment gets a 404 from the router, not a 422 from validation.",{"q":2502,"a":2503},"Where should a constraint live — on Query or on the Pydantic model?","Put it on the model when the rule belongs to the data and should appear wherever that data is used. Put it on Query or Path when the rule is about this endpoint's interface, such as a pagination ceiling that other callers of the same model do not share.",null,{"slug":2506,"breadcrumb":2507},"query-path-and-body-parameter-validation",[2508,2510,2513,2514],{"label":2509,"path":887},"Home",{"label":2511,"path":2512},"Advanced Pydantic Validation & Serialization","\u002Fadvanced-pydantic-validation-serialization\u002F",{"label":678,"path":677},{"label":2515,"path":2516},"Query, Path and Body Parameter Validation","\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fquery-path-and-body-parameter-validation\u002F",{"title":163,"description":2489},"article","hhVq3liLBFNnZ9X6DqBZRkZIqZYoZsUUCsKGjOpN_R4",[2504,2504],1784588202620]