[{"data":1,"prerenderedAt":2868},["ShallowReactive",2],{"nav":3,"page-\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fvalidating-file-uploads-and-forms\u002F":580,"surround-\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fvalidating-file-uploads-and-forms\u002F":2867},[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":169,"body":582,"dateModified":2834,"datePublished":2834,"description":2835,"extension":2836,"faq":2837,"howto":2850,"meta":2851,"navigation":1142,"path":170,"seo":2864,"stem":171,"type":2865,"__hash__":2866},"content\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fvalidating-file-uploads-and-forms\u002Findex.md",{"type":583,"value":584,"toc":2820},"minimark",[585,589,596,658,672,686,691,717,897,902,917,926,929,1229,1232,1239,1252,1265,1276,1282,1286,1289,1502,1508,1515,1518,1530,1539,1678,1693,1699,1703,1711,1893,1899,1905,1909,1916,2091,2100,2106,2113,2116,2226,2232,2239,2243,2259,2262,2320,2323,2329,2338,2368,2372,2383,2633,2640,2644,2650,2661,2678,2690,2694,2720,2737,2752,2761,2773,2777,2816],[586,587,169],"h1",{"id":588},"validating-file-uploads-and-forms-in-fastapi",[590,591,592],"p",{},[593,594,595],"strong",{},"Key takeaways:",[597,598,599,619,640,645,652],"ul",{},[600,601,602,606,607,610,611,614,615,618],"li",{},[603,604,605],"code",{},"UploadFile"," spools to disk and exposes ",[603,608,609],{},"filename"," and ",[603,612,613],{},"content_type","; ",[603,616,617],{},"bytes = File()"," buffers the whole upload in memory first.",[600,620,621,624,625,610,628,631,632,635,636,639],{},[603,622,623],{},"Form"," fields carry the same Pydantic constraints as ",[603,626,627],{},"Query",[603,629,630],{},"Body",", and their errors use the ",[603,633,634],{},"body"," prefix in ",[603,637,638],{},"loc",".",[600,641,642,644],{},[603,643,613],{}," is client-supplied metadata, not evidence — check the bytes before you trust it.",[600,646,647,648,651],{},"Size limits need enforcement in the handler as you read, not just a ",[603,649,650],{},"Content-Length"," check.",[600,653,654,655,657],{},"A JSON body parameter and a ",[603,656,623],{}," field cannot both be satisfied by one request; the endpoint 422s either way.",[590,659,660,661,666,667,671],{},"This page continues ",[662,663,665],"a",{"href":664},"\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002F","Request Validation Patterns"," into non-JSON payloads. The rules from ",[662,668,670],{"href":669},"\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fquery-path-and-body-parameter-validation\u002F","query, path and body parameter validation"," still apply — including the multiple-body-parameter rule, which bites in a new way here.",[590,673,674,675,678,679,681,682,685],{},"Uploads require ",[603,676,677],{},"python-multipart"," to be installed; without it FastAPI raises at import time when it first sees a ",[603,680,623],{}," or ",[603,683,684],{},"File"," parameter.",[687,688,690],"h3",{"id":689},"how-these-transcripts-were-produced","How these transcripts were produced",[590,692,693,694,697,698,701,702,705,706,709,710,713,714,716],{},"The site's verification harness sends JSON bodies through tuples of ",[603,695,696],{},"(method, path, json)",", which cannot express a multipart request. Rather than hand-write the output — which this site never does — the example app in ",[603,699,700],{},"_verify\u002Fexamples\u002Fval-uploads-forms.py"," drives itself: a ",[603,703,704],{},"\u002Fselftest"," endpoint builds genuine ",[603,707,708],{},"multipart\u002Fform-data"," requests with httpx and posts them back into the same app through ",[603,711,712],{},"ASGITransport",", then returns each real status code and response body. The harness calls ",[603,715,704],{}," once, so every status and every 422 below travelled through a real multipart parse. The environment is FastAPI 0.139.2, Pydantic 2.13.4, python-multipart 0.0.32 and Python 3.12.",[718,719,720,893],"figure",{},[721,722,730,731,730,735,730,739,730,748,730,755,730,760,730,764,730,772,730,778,730,782,730,785,730,789,730,793,730,800,730,806,730,810,730,815,730,818,730,821,730,825,730,829,730,833,730,837,730,844,730,849,730,852,730,855,730,861,730,865,730,869,730,872,730,876,730,879,730,885,730,889],"svg",{"viewBox":723,"role":724,"ariaLabelledBy":725,"xmlns":728,"style":729},"0 0 720 320","img",[726,727],"upl-title","upl-desc","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;max-width:720px;margin:2rem 0","\n  ",[732,733,734],"title",{"id":726},"How a multipart request is split into form fields and files",[736,737,738],"desc",{"id":727},"One multipart body is parsed into named parts. Text parts bind to Form parameters and are validated by pydantic-core. File parts bind to UploadFile, which spools to memory then disk, or to bytes, which is fully buffered in memory.",[740,741],"rect",{"x":742,"y":743,"width":744,"height":745,"rx":746,"style":747},"18","24","180","80","8","fill:#F9FAFB;stroke:currentColor;stroke-width:1.4px",[749,750,754],"text",{"x":751,"y":752,"style":753},"108","52","text-anchor:middle;fill:currentColor;font:600 13px sans-serif","multipart body",[749,756,759],{"x":751,"y":757,"style":758},"72","text-anchor:middle;fill:currentColor;font:400 11px sans-serif","one Content-Type,",[749,761,763],{"x":751,"y":762,"style":758},"89","many named parts",[740,765],{"x":766,"y":767,"width":768,"height":769,"rx":770,"style":771},"250","12","190","46","7","fill:#FFFFFF;stroke:#00796B;stroke-width:1.5px",[749,773,777],{"x":774,"y":775,"style":776},"345","32","text-anchor:middle;fill:#00796B;font:600 12.5px sans-serif","text part",[749,779,781],{"x":774,"y":780,"style":758},"49","binds to Form(...)",[740,783],{"x":766,"y":784,"width":768,"height":769,"rx":770,"style":771},"76",[749,786,788],{"x":774,"y":787,"style":776},"96","file part",[749,790,792],{"x":774,"y":791,"style":758},"113","has filename + type",[740,794],{"x":795,"y":796,"width":797,"height":798,"rx":746,"style":799},"492","4","210","60","fill:#E0F2F1;stroke:#00796B;stroke-width:1.5px",[749,801,805],{"x":802,"y":803,"style":804},"597","26","text-anchor:middle;fill:#00796B;font:700 12.5px sans-serif","pydantic-core",[749,807,809],{"x":802,"y":808,"style":758},"45","coerce + constrain",[749,811,814],{"x":802,"y":812,"style":813},"59","text-anchor:middle;fill:currentColor;font:400 10.5px sans-serif","errors use loc: body",[740,816],{"x":795,"y":745,"width":797,"height":817,"rx":746,"style":799},"48",[749,819,605],{"x":802,"y":820,"style":804},"101",[749,822,824],{"x":802,"y":823,"style":758},"119","spooled: RAM then disk",[740,826],{"x":795,"y":827,"width":797,"height":817,"rx":746,"style":828},"144","fill:#FFFFFF;stroke:currentColor;stroke-width:1.4px",[749,830,617],{"x":802,"y":831,"style":832},"165","text-anchor:middle;fill:currentColor;font:700 12.5px sans-serif",[749,834,836],{"x":802,"y":835,"style":758},"183","fully buffered in RAM",[838,839],"line",{"x1":840,"y1":752,"x2":841,"y2":842,"style":843},"198","244","35","stroke:currentColor;stroke-width:1.4px",[845,846],"polygon",{"points":847,"style":848},"240,30 248,34 241,40","fill:currentColor",[838,850],{"x1":840,"y1":784,"x2":841,"y2":851,"style":843},"99",[845,853],{"points":854,"style":848},"240,94 248,100 240,105",[838,856],{"x1":857,"y1":842,"x2":858,"y2":859,"style":860},"440","486","34","stroke:#00796B;stroke-width:1.5px",[845,862],{"points":863,"style":864},"482,30 490,34 482,38","fill:#00796B",[838,866],{"x1":857,"y1":867,"x2":858,"y2":868,"style":860},"95","100",[845,870],{"points":871,"style":864},"482,96 490,100 482,105",[838,873],{"x1":857,"y1":874,"x2":858,"y2":875,"style":843},"110","162",[845,877],{"points":878,"style":848},"482,157 490,163 481,167",[749,880,884],{"x":881,"y":882,"style":883},"360","240","text-anchor:middle;fill:currentColor;font:400 12px sans-serif","A request carries exactly one body. Multipart and application\u002Fjson",[749,886,888],{"x":881,"y":887,"style":883},"260","are mutually exclusive, so Form fields and a JSON model cannot",[749,890,892],{"x":881,"y":891,"style":883},"280","both be satisfied by the same request.",[894,895,896],"figcaption",{},"Text parts and file parts come out of one multipart parse. The choice between UploadFile and bytes decides whether the payload lands on disk or in RAM.",[898,899,901],"h2",{"id":900},"the-problem-this-solves","The Problem This Solves",[590,903,904,905,908,909,912,913,916],{},"An avatar upload endpoint works fine in development against 20 KB PNGs. In production someone posts a 400 MB video with the filename ",[603,906,907],{},"avatar.png"," and the ",[603,910,911],{},"Content-Type"," header set to ",[603,914,915],{},"image\u002Fpng",", and your worker's RSS goes vertical. Everything that went wrong there is a validation question: how big, what kind, and how do you find out before you have already allocated the memory.",[898,918,920,922,923],{"id":919},"uploadfile-versus-bytes",[603,921,605],{}," versus ",[603,924,925],{},"bytes",[590,927,928],{},"The two declarations look interchangeable and are not:",[930,931,936],"pre",{"className":932,"code":933,"language":934,"meta":935,"style":935},"language-python shiki shiki-themes github-light-high-contrast","@app.post(\"\u002Favatar\u002Fstream\")\nasync def avatar_stream(file: UploadFile) -> dict[str, Any]:\n    # UploadFile is a spooled file: metadata is available without reading the payload.\n    head = await file.read(8)\n    await file.seek(0)\n    body = await file.read()\n    return {\n        \"filename\": file.filename,\n        \"content_type\": file.content_type,\n        \"size\": len(body),\n        \"first_8_bytes\": head.decode(\"latin-1\"),\n        \"type\": type(file).__name__,\n    }\n\n\n@app.post(\"\u002Favatar\u002Fbytes\")\nasync def avatar_bytes(file: Annotated[bytes, File()]) -> dict[str, Any]:\n    # bytes = File() buffers the WHOLE upload in memory before the handler runs.\n    return {\"size\": len(file), \"type\": type(file).__name__}\n","python","",[603,937,938,957,980,987,1010,1026,1041,1050,1065,1078,1092,1107,1131,1137,1144,1149,1161,1183,1189],{"__ignoreMap":935},[939,940,942,946,950,954],"span",{"class":838,"line":941},1,[939,943,945],{"class":944},"s3dhs","@app.post",[939,947,949],{"class":948},"sigWx","(",[939,951,953],{"class":952},"sYEJz","\"\u002Favatar\u002Fstream\"",[939,955,956],{"class":948},")\n",[939,958,960,964,967,970,973,977],{"class":838,"line":959},2,[939,961,963],{"class":962},"sTJeM","async",[939,965,966],{"class":962}," def",[939,968,969],{"class":944}," avatar_stream",[939,971,972],{"class":948},"(file: UploadFile) -> dict[",[939,974,976],{"class":975},"sacAq","str",[939,978,979],{"class":948},", Any]:\n",[939,981,983],{"class":838,"line":982},3,[939,984,986],{"class":985},"sFeEa","    # UploadFile is a spooled file: metadata is available without reading the payload.\n",[939,988,990,993,996,999,1003,1006,1008],{"class":838,"line":989},4,[939,991,992],{"class":948},"    head ",[939,994,995],{"class":962},"=",[939,997,998],{"class":962}," await",[939,1000,1002],{"class":1001},"sV4o_"," file",[939,1004,1005],{"class":948},".read(",[939,1007,746],{"class":975},[939,1009,956],{"class":948},[939,1011,1013,1016,1018,1021,1024],{"class":838,"line":1012},5,[939,1014,1015],{"class":962},"    await",[939,1017,1002],{"class":1001},[939,1019,1020],{"class":948},".seek(",[939,1022,1023],{"class":975},"0",[939,1025,956],{"class":948},[939,1027,1029,1032,1034,1036,1038],{"class":838,"line":1028},6,[939,1030,1031],{"class":948},"    body ",[939,1033,995],{"class":962},[939,1035,998],{"class":962},[939,1037,1002],{"class":1001},[939,1039,1040],{"class":948},".read()\n",[939,1042,1044,1047],{"class":838,"line":1043},7,[939,1045,1046],{"class":962},"    return",[939,1048,1049],{"class":948}," {\n",[939,1051,1053,1056,1059,1062],{"class":838,"line":1052},8,[939,1054,1055],{"class":952},"        \"filename\"",[939,1057,1058],{"class":948},": ",[939,1060,1061],{"class":1001},"file",[939,1063,1064],{"class":948},".filename,\n",[939,1066,1068,1071,1073,1075],{"class":838,"line":1067},9,[939,1069,1070],{"class":952},"        \"content_type\"",[939,1072,1058],{"class":948},[939,1074,1061],{"class":1001},[939,1076,1077],{"class":948},".content_type,\n",[939,1079,1081,1084,1086,1089],{"class":838,"line":1080},10,[939,1082,1083],{"class":952},"        \"size\"",[939,1085,1058],{"class":948},[939,1087,1088],{"class":975},"len",[939,1090,1091],{"class":948},"(body),\n",[939,1093,1095,1098,1101,1104],{"class":838,"line":1094},11,[939,1096,1097],{"class":952},"        \"first_8_bytes\"",[939,1099,1100],{"class":948},": head.decode(",[939,1102,1103],{"class":952},"\"latin-1\"",[939,1105,1106],{"class":948},"),\n",[939,1108,1110,1113,1115,1118,1120,1122,1125,1128],{"class":838,"line":1109},12,[939,1111,1112],{"class":952},"        \"type\"",[939,1114,1058],{"class":948},[939,1116,1117],{"class":975},"type",[939,1119,949],{"class":948},[939,1121,1061],{"class":1001},[939,1123,1124],{"class":948},").",[939,1126,1127],{"class":975},"__name__",[939,1129,1130],{"class":948},",\n",[939,1132,1134],{"class":838,"line":1133},13,[939,1135,1136],{"class":948},"    }\n",[939,1138,1140],{"class":838,"line":1139},14,[939,1141,1143],{"emptyLinePlaceholder":1142},true,"\n",[939,1145,1147],{"class":838,"line":1146},15,[939,1148,1143],{"emptyLinePlaceholder":1142},[939,1150,1152,1154,1156,1159],{"class":838,"line":1151},16,[939,1153,945],{"class":944},[939,1155,949],{"class":948},[939,1157,1158],{"class":952},"\"\u002Favatar\u002Fbytes\"",[939,1160,956],{"class":948},[939,1162,1164,1166,1168,1171,1174,1176,1179,1181],{"class":838,"line":1163},17,[939,1165,963],{"class":962},[939,1167,966],{"class":962},[939,1169,1170],{"class":944}," avatar_bytes",[939,1172,1173],{"class":948},"(file: Annotated[",[939,1175,925],{"class":975},[939,1177,1178],{"class":948},", File()]) -> dict[",[939,1180,976],{"class":975},[939,1182,979],{"class":948},[939,1184,1186],{"class":838,"line":1185},18,[939,1187,1188],{"class":985},"    # bytes = File() buffers the WHOLE upload in memory before the handler runs.\n",[939,1190,1192,1194,1197,1200,1202,1204,1206,1208,1211,1214,1216,1218,1220,1222,1224,1226],{"class":838,"line":1191},19,[939,1193,1046],{"class":962},[939,1195,1196],{"class":948}," {",[939,1198,1199],{"class":952},"\"size\"",[939,1201,1058],{"class":948},[939,1203,1088],{"class":975},[939,1205,949],{"class":948},[939,1207,1061],{"class":1001},[939,1209,1210],{"class":948},"), ",[939,1212,1213],{"class":952},"\"type\"",[939,1215,1058],{"class":948},[939,1217,1117],{"class":975},[939,1219,949],{"class":948},[939,1221,1061],{"class":1001},[939,1223,1124],{"class":948},[939,1225,1127],{"class":975},[939,1227,1228],{"class":948},"}\n",[590,1230,1231],{},"Real output from the self-driven multipart run:",[930,1233,1237],{"className":1234,"code":1236,"language":749,"meta":935},[1235],"language-text","  {\n    \"request\": \"POST \u002Favatar\u002Fstream  (png, 32 bytes)\",\n    \"status\": 200,\n    \"response\": {\n      \"filename\": \"logo.png\",\n      \"content_type\": \"image\u002Fpng\",\n      \"size\": 32,\n      \"first_8_bytes\": \"\\u0089PNG\\r\\n\\u001a\\n\",\n      \"type\": \"UploadFile\"\n    }\n  },\n  {\n    \"request\": \"POST \u002Favatar\u002Fbytes  (png, 32 bytes)\",\n    \"status\": 200,\n    \"response\": {\n      \"size\": 32,\n      \"type\": \"bytes\"\n    }\n  },\n",[603,1238,1236],{"__ignoreMap":935},[590,1240,1241,1242,1244,1245,1247,1248,1251],{},"The ",[603,1243,925],{}," version knows the size and nothing else — no filename, no declared content type, and no opportunity to stop reading. Starlette had already materialised the whole payload before the handler was entered. ",[603,1246,605],{}," wraps a ",[603,1249,1250],{},"SpooledTemporaryFile",": it keeps small uploads in memory and rolls over to a temporary file past a threshold, which is why a 400 MB upload becomes disk pressure rather than an OOM kill.",[590,1253,1254,1256,1257,610,1259,1261,1262,1264],{},[603,1255,605],{}," is also where ",[603,1258,609],{},[603,1260,613],{}," live, and both are needed for any meaningful check. Prefer it by default. ",[603,1263,617],{}," is reasonable only for payloads you have already bounded elsewhere — a signature blob, a small avatar behind a proxy-enforced limit.",[590,1266,1267,1268,1270,1271,1275],{},"A missing file part produces an ordinary validation error, with the ",[603,1269,634],{}," prefix because multipart ",[1272,1273,1274],"em",{},"is"," the body:",[930,1277,1280],{"className":1278,"code":1279,"language":749,"meta":935},[1235],"  {\n    \"request\": \"POST \u002Favatar\u002Fstream  (no file part at all)\",\n    \"status\": 422,\n    \"response\": {\n      \"detail\": [\n        {\n          \"type\": \"missing\",\n          \"loc\": [\n            \"body\",\n            \"file\"\n          ],\n          \"msg\": \"Field required\",\n          \"input\": null\n        }\n      ]\n    }\n  },\n",[603,1281,1279],{"__ignoreMap":935},[898,1283,1285],{"id":1284},"content-type-and-size-checks","Content-Type and Size Checks",[590,1287,1288],{},"Both checks belong in the handler, and both return a status other than 422, because neither is a schema violation:",[930,1290,1292],{"className":932,"code":1291,"language":934,"meta":935,"style":935},"MAX_BYTES = 64          # deliberately tiny so the limit is easy to trip in a transcript\nALLOWED = {\"image\u002Fpng\", \"image\u002Fjpeg\"}\n\n\n@app.post(\"\u002Favatar\u002Fchecked\")\nasync def avatar_checked(file: UploadFile) -> dict[str, Any]:\n    if file.content_type not in ALLOWED:\n        raise HTTPException(415, f\"unsupported content type {file.content_type!r}\")\n    body = await file.read()\n    if len(body) > MAX_BYTES:\n        raise HTTPException(413, f\"file exceeds {MAX_BYTES} bytes\")\n    return {\"stored\": file.filename, \"size\": len(body)}\n",[603,1293,1294,1308,1328,1332,1336,1347,1362,1384,1419,1431,1449,1477],{"__ignoreMap":935},[939,1295,1296,1299,1302,1305],{"class":838,"line":941},[939,1297,1298],{"class":975},"MAX_BYTES",[939,1300,1301],{"class":962}," =",[939,1303,1304],{"class":975}," 64",[939,1306,1307],{"class":985},"          # deliberately tiny so the limit is easy to trip in a transcript\n",[939,1309,1310,1313,1315,1317,1320,1323,1326],{"class":838,"line":959},[939,1311,1312],{"class":975},"ALLOWED",[939,1314,1301],{"class":962},[939,1316,1196],{"class":948},[939,1318,1319],{"class":952},"\"image\u002Fpng\"",[939,1321,1322],{"class":948},", ",[939,1324,1325],{"class":952},"\"image\u002Fjpeg\"",[939,1327,1228],{"class":948},[939,1329,1330],{"class":838,"line":982},[939,1331,1143],{"emptyLinePlaceholder":1142},[939,1333,1334],{"class":838,"line":989},[939,1335,1143],{"emptyLinePlaceholder":1142},[939,1337,1338,1340,1342,1345],{"class":838,"line":1012},[939,1339,945],{"class":944},[939,1341,949],{"class":948},[939,1343,1344],{"class":952},"\"\u002Favatar\u002Fchecked\"",[939,1346,956],{"class":948},[939,1348,1349,1351,1353,1356,1358,1360],{"class":838,"line":1028},[939,1350,963],{"class":962},[939,1352,966],{"class":962},[939,1354,1355],{"class":944}," avatar_checked",[939,1357,972],{"class":948},[939,1359,976],{"class":975},[939,1361,979],{"class":948},[939,1363,1364,1367,1369,1372,1375,1378,1381],{"class":838,"line":1043},[939,1365,1366],{"class":962},"    if",[939,1368,1002],{"class":1001},[939,1370,1371],{"class":948},".content_type ",[939,1373,1374],{"class":962},"not",[939,1376,1377],{"class":962}," in",[939,1379,1380],{"class":975}," ALLOWED",[939,1382,1383],{"class":948},":\n",[939,1385,1386,1389,1392,1395,1397,1400,1403,1406,1408,1411,1414,1417],{"class":838,"line":1052},[939,1387,1388],{"class":962},"        raise",[939,1390,1391],{"class":948}," HTTPException(",[939,1393,1394],{"class":975},"415",[939,1396,1322],{"class":948},[939,1398,1399],{"class":962},"f",[939,1401,1402],{"class":952},"\"unsupported content type ",[939,1404,1405],{"class":962},"{",[939,1407,1061],{"class":1001},[939,1409,1410],{"class":948},".content_type",[939,1412,1413],{"class":962},"!r}",[939,1415,1416],{"class":952},"\"",[939,1418,956],{"class":948},[939,1420,1421,1423,1425,1427,1429],{"class":838,"line":1067},[939,1422,1031],{"class":948},[939,1424,995],{"class":962},[939,1426,998],{"class":962},[939,1428,1002],{"class":1001},[939,1430,1040],{"class":948},[939,1432,1433,1435,1438,1441,1444,1447],{"class":838,"line":1080},[939,1434,1366],{"class":962},[939,1436,1437],{"class":975}," len",[939,1439,1440],{"class":948},"(body) ",[939,1442,1443],{"class":962},">",[939,1445,1446],{"class":975}," MAX_BYTES",[939,1448,1383],{"class":948},[939,1450,1451,1453,1455,1458,1460,1462,1465,1467,1469,1472,1475],{"class":838,"line":1094},[939,1452,1388],{"class":962},[939,1454,1391],{"class":948},[939,1456,1457],{"class":975},"413",[939,1459,1322],{"class":948},[939,1461,1399],{"class":962},[939,1463,1464],{"class":952},"\"file exceeds ",[939,1466,1405],{"class":962},[939,1468,1298],{"class":975},[939,1470,1471],{"class":962},"}",[939,1473,1474],{"class":952}," bytes\"",[939,1476,956],{"class":948},[939,1478,1479,1481,1483,1486,1488,1490,1493,1495,1497,1499],{"class":838,"line":1109},[939,1480,1046],{"class":962},[939,1482,1196],{"class":948},[939,1484,1485],{"class":952},"\"stored\"",[939,1487,1058],{"class":948},[939,1489,1061],{"class":1001},[939,1491,1492],{"class":948},".filename, ",[939,1494,1199],{"class":952},[939,1496,1058],{"class":948},[939,1498,1088],{"class":975},[939,1500,1501],{"class":948},"(body)}\n",[930,1503,1506],{"className":1504,"code":1505,"language":749,"meta":935},[1235],"  {\n    \"request\": \"POST \u002Favatar\u002Fchecked  (text\u002Fplain)\",\n    \"status\": 415,\n    \"response\": {\n      \"detail\": \"unsupported content type 'text\u002Fplain'\"\n    }\n  },\n  {\n    \"request\": \"POST \u002Favatar\u002Fchecked  (png, 4096 bytes)\",\n    \"status\": 413,\n    \"response\": {\n      \"detail\": \"file exceeds 64 bytes\"\n    }\n  },\n",[603,1507,1505],{"__ignoreMap":935},[590,1509,1510,1511,639],{},"415 and 413 are the right codes — the media type is unsupported, the payload is too large — and both are more actionable for a client than a generic 400. Consistent mapping of these to your error envelope is the job of a ",[662,1512,1514],{"href":1513},"\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fglobal-exception-handlers-for-consistent-api-responses\u002F","global exception handler",[590,1516,1517],{},"Two honest caveats about the code above.",[590,1519,1520,1525,1526,1529],{},[593,1521,1522,1524],{},[603,1523,613],{}," is a claim, not a fact."," It is whatever the client wrote in the part header. ",[603,1527,1528],{},"curl -F \"file=@virus.exe;type=image\u002Fpng\""," sails through. Treat it as a cheap first filter that rejects the accidental cases, then verify the bytes. The header check above is worth keeping precisely because it is cheap — it rejects most mistakes before you read anything — but it must be followed by a magic-number check on the first few bytes, and by real processing (opening the image, for instance) before you store or serve the file.",[590,1531,1532,1538],{},[593,1533,1534,1537],{},[603,1535,1536],{},"await file.read()"," then checking the length is too late for a big file."," By the time the check runs, the bytes are already spooled. For a genuine ceiling, read in chunks and abort as you go:",[930,1540,1542],{"className":932,"code":1541,"language":934,"meta":935,"style":935},"async def read_bounded(file: UploadFile, limit: int) -> bytes:\n    chunks, total = [], 0\n    while chunk := await file.read(64 * 1024):\n        total += len(chunk)\n        if total > limit:\n            raise HTTPException(413, f\"file exceeds {limit} bytes\")\n        chunks.append(chunk)\n    return b\"\".join(chunks)\n",[603,1543,1544,1566,1579,1608,1621,1634,1660,1665],{"__ignoreMap":935},[939,1545,1546,1548,1550,1553,1556,1559,1562,1564],{"class":838,"line":941},[939,1547,963],{"class":962},[939,1549,966],{"class":962},[939,1551,1552],{"class":944}," read_bounded",[939,1554,1555],{"class":948},"(file: UploadFile, limit: ",[939,1557,1558],{"class":975},"int",[939,1560,1561],{"class":948},") -> ",[939,1563,925],{"class":975},[939,1565,1383],{"class":948},[939,1567,1568,1571,1573,1576],{"class":838,"line":959},[939,1569,1570],{"class":948},"    chunks, total ",[939,1572,995],{"class":962},[939,1574,1575],{"class":948}," [], ",[939,1577,1578],{"class":975},"0\n",[939,1580,1581,1584,1587,1590,1592,1594,1596,1599,1602,1605],{"class":838,"line":982},[939,1582,1583],{"class":962},"    while",[939,1585,1586],{"class":948}," chunk ",[939,1588,1589],{"class":962},":=",[939,1591,998],{"class":962},[939,1593,1002],{"class":1001},[939,1595,1005],{"class":948},[939,1597,1598],{"class":975},"64",[939,1600,1601],{"class":962}," *",[939,1603,1604],{"class":975}," 1024",[939,1606,1607],{"class":948},"):\n",[939,1609,1610,1613,1616,1618],{"class":838,"line":989},[939,1611,1612],{"class":948},"        total ",[939,1614,1615],{"class":962},"+=",[939,1617,1437],{"class":975},[939,1619,1620],{"class":948},"(chunk)\n",[939,1622,1623,1626,1629,1631],{"class":838,"line":1012},[939,1624,1625],{"class":962},"        if",[939,1627,1628],{"class":948}," total ",[939,1630,1443],{"class":962},[939,1632,1633],{"class":948}," limit:\n",[939,1635,1636,1639,1641,1643,1645,1647,1649,1651,1654,1656,1658],{"class":838,"line":1028},[939,1637,1638],{"class":962},"            raise",[939,1640,1391],{"class":948},[939,1642,1457],{"class":975},[939,1644,1322],{"class":948},[939,1646,1399],{"class":962},[939,1648,1464],{"class":952},[939,1650,1405],{"class":962},[939,1652,1653],{"class":948},"limit",[939,1655,1471],{"class":962},[939,1657,1474],{"class":952},[939,1659,956],{"class":948},[939,1661,1662],{"class":838,"line":1043},[939,1663,1664],{"class":948},"        chunks.append(chunk)\n",[939,1666,1667,1669,1672,1675],{"class":838,"line":1052},[939,1668,1046],{"class":962},[939,1670,1671],{"class":962}," b",[939,1673,1674],{"class":952},"\"\"",[939,1676,1677],{"class":948},".join(chunks)\n",[590,1679,1680,1681,1684,1685,1688,1689,1692],{},"That still lets a client stream ",[603,1682,1683],{},"limit + 64 KB"," before you cut them off, which is fine. What it does not do is bound the request at the socket. Do that at the edge — ",[603,1686,1687],{},"client_max_body_size"," in nginx, ",[603,1690,1691],{},"--limit-request-body"," or its equivalent in your ASGI server — so that abusive requests never reach Python at all. The handler check is your second line of defence and the one that produces a useful error message.",[590,1694,1695,1696,1698],{},"Also note the ",[603,1697,650],{}," trap: a chunked request need not send one, and a client can send one that lies. It is a hint for fast rejection, never the enforcement mechanism.",[898,1700,1702],{"id":1701},"mixing-form-fields-and-files","Mixing Form Fields and Files",[590,1704,1705,1706,610,1708,1710],{},"Form fields take the same constraints as ",[603,1707,627],{},[603,1709,630],{},":",[930,1712,1714],{"className":932,"code":1713,"language":934,"meta":935,"style":935},"@app.post(\"\u002Fprofiles\u002F\")\nasync def create_profile(\n    display_name: Annotated[str, Form(min_length=2, max_length=40)],\n    age: Annotated[int, Form(ge=13)],\n    avatar: UploadFile,\n    bio: Annotated[str | None, Form()] = None,\n) -> dict[str, Any]:\n    body = await avatar.read()\n    return {\n        \"display_name\": display_name,\n        \"age\": age,\n        \"bio\": bio,\n        \"avatar\": {\"filename\": avatar.filename, \"size\": len(body)},\n    }\n",[603,1715,1716,1727,1739,1770,1789,1794,1816,1825,1836,1842,1850,1858,1866,1889],{"__ignoreMap":935},[939,1717,1718,1720,1722,1725],{"class":838,"line":941},[939,1719,945],{"class":944},[939,1721,949],{"class":948},[939,1723,1724],{"class":952},"\"\u002Fprofiles\u002F\"",[939,1726,956],{"class":948},[939,1728,1729,1731,1733,1736],{"class":838,"line":959},[939,1730,963],{"class":962},[939,1732,966],{"class":962},[939,1734,1735],{"class":944}," create_profile",[939,1737,1738],{"class":948},"(\n",[939,1740,1741,1744,1746,1749,1752,1754,1757,1759,1762,1764,1767],{"class":838,"line":982},[939,1742,1743],{"class":948},"    display_name: Annotated[",[939,1745,976],{"class":975},[939,1747,1748],{"class":948},", Form(",[939,1750,1751],{"class":1001},"min_length",[939,1753,995],{"class":962},[939,1755,1756],{"class":975},"2",[939,1758,1322],{"class":948},[939,1760,1761],{"class":1001},"max_length",[939,1763,995],{"class":962},[939,1765,1766],{"class":975},"40",[939,1768,1769],{"class":948},")],\n",[939,1771,1772,1775,1777,1779,1782,1784,1787],{"class":838,"line":989},[939,1773,1774],{"class":948},"    age: Annotated[",[939,1776,1558],{"class":975},[939,1778,1748],{"class":948},[939,1780,1781],{"class":1001},"ge",[939,1783,995],{"class":962},[939,1785,1786],{"class":975},"13",[939,1788,1769],{"class":948},[939,1790,1791],{"class":838,"line":1012},[939,1792,1793],{"class":948},"    avatar: UploadFile,\n",[939,1795,1796,1799,1801,1804,1807,1810,1812,1814],{"class":838,"line":1028},[939,1797,1798],{"class":948},"    bio: Annotated[",[939,1800,976],{"class":975},[939,1802,1803],{"class":962}," |",[939,1805,1806],{"class":975}," None",[939,1808,1809],{"class":948},", Form()] ",[939,1811,995],{"class":962},[939,1813,1806],{"class":975},[939,1815,1130],{"class":948},[939,1817,1818,1821,1823],{"class":838,"line":1043},[939,1819,1820],{"class":948},") -> dict[",[939,1822,976],{"class":975},[939,1824,979],{"class":948},[939,1826,1827,1829,1831,1833],{"class":838,"line":1052},[939,1828,1031],{"class":948},[939,1830,995],{"class":962},[939,1832,998],{"class":962},[939,1834,1835],{"class":948}," avatar.read()\n",[939,1837,1838,1840],{"class":838,"line":1067},[939,1839,1046],{"class":962},[939,1841,1049],{"class":948},[939,1843,1844,1847],{"class":838,"line":1080},[939,1845,1846],{"class":952},"        \"display_name\"",[939,1848,1849],{"class":948},": display_name,\n",[939,1851,1852,1855],{"class":838,"line":1094},[939,1853,1854],{"class":952},"        \"age\"",[939,1856,1857],{"class":948},": age,\n",[939,1859,1860,1863],{"class":838,"line":1109},[939,1861,1862],{"class":952},"        \"bio\"",[939,1864,1865],{"class":948},": bio,\n",[939,1867,1868,1871,1874,1877,1880,1882,1884,1886],{"class":838,"line":1133},[939,1869,1870],{"class":952},"        \"avatar\"",[939,1872,1873],{"class":948},": {",[939,1875,1876],{"class":952},"\"filename\"",[939,1878,1879],{"class":948},": avatar.filename, ",[939,1881,1199],{"class":952},[939,1883,1058],{"class":948},[939,1885,1088],{"class":975},[939,1887,1888],{"class":948},"(body)},\n",[939,1890,1891],{"class":838,"line":1139},[939,1892,1136],{"class":948},[930,1894,1897],{"className":1895,"code":1896,"language":749,"meta":935},[1235],"  {\n    \"request\": \"POST \u002Fprofiles\u002F  (valid form + file)\",\n    \"status\": 200,\n    \"response\": {\n      \"display_name\": \"Ada\",\n      \"age\": 36,\n      \"bio\": \"Engineer\",\n      \"avatar\": {\n        \"filename\": \"me.png\",\n        \"size\": 20\n      }\n    }\n  },\n  {\n    \"request\": \"POST \u002Fprofiles\u002F  (age=9, display_name too short, no avatar)\",\n    \"status\": 422,\n    \"response\": {\n      \"detail\": [\n        {\n          \"type\": \"string_too_short\",\n          \"loc\": [\n            \"body\",\n            \"display_name\"\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            \"body\",\n            \"age\"\n          ],\n          \"msg\": \"Input should be greater than or equal to 13\",\n          \"input\": \"9\",\n          \"ctx\": {\n            \"ge\": 13\n          }\n        },\n        {\n          \"type\": \"missing\",\n          \"loc\": [\n            \"body\",\n            \"avatar\"\n          ],\n          \"msg\": \"Field required\",\n          \"input\": null\n        }\n      ]\n    }\n  },\n",[603,1898,1896],{"__ignoreMap":935},[590,1900,1901,1904],{},[603,1902,1903],{},"\"input\": \"9\""," is a string, because multipart is a text protocol like the query string — every value arrives as text and is coerced. And all three failures, including the missing file, accumulate into one response, exactly as they do for JSON.",[898,1906,1908],{"id":1907},"the-form-model-trap","The Form-Model Trap",[590,1910,1911,1912,1915],{},"FastAPI 0.113 and later accept a Pydantic model bound to ",[603,1913,1914],{},"Form()",", which is a genuinely nice way to reuse a validated shape across a form endpoint and a JSON one. It has a sharp edge:",[930,1917,1919],{"className":932,"code":1918,"language":934,"meta":935,"style":935},"class Metadata(BaseModel):\n    title: str\n    tags: list[str] = []\n\n\n@app.post(\"\u002Fdocuments\u002Fmeta-only\")\nasync def document_meta(metadata: Annotated[Metadata, Form()]) -> dict[str, Any]:\n    # Its fields are read FLAT from the form, as long as it is the ONLY body parameter.\n    return metadata.model_dump()\n\n\n@app.post(\"\u002Fdocuments\u002Fwith-file\")\nasync def document_with_file(\n    metadata: Annotated[Metadata, Form()],\n    file: UploadFile,\n) -> dict[str, Any]:\n    # Adding a second body parameter makes FastAPI embed the model under its own name.\n    body = await file.read()\n    return {\"metadata\": metadata.model_dump(), \"size\": len(body)}\n",[603,1920,1921,1936,1944,1959,1963,1967,1978,1994,1999,2006,2010,2014,2025,2036,2041,2046,2054,2059,2071],{"__ignoreMap":935},[939,1922,1923,1926,1929,1931,1934],{"class":838,"line":941},[939,1924,1925],{"class":962},"class",[939,1927,1928],{"class":1001}," Metadata",[939,1930,949],{"class":948},[939,1932,1933],{"class":975},"BaseModel",[939,1935,1607],{"class":948},[939,1937,1938,1941],{"class":838,"line":959},[939,1939,1940],{"class":948},"    title: ",[939,1942,1943],{"class":975},"str\n",[939,1945,1946,1949,1951,1954,1956],{"class":838,"line":982},[939,1947,1948],{"class":948},"    tags: list[",[939,1950,976],{"class":975},[939,1952,1953],{"class":948},"] ",[939,1955,995],{"class":962},[939,1957,1958],{"class":948}," []\n",[939,1960,1961],{"class":838,"line":989},[939,1962,1143],{"emptyLinePlaceholder":1142},[939,1964,1965],{"class":838,"line":1012},[939,1966,1143],{"emptyLinePlaceholder":1142},[939,1968,1969,1971,1973,1976],{"class":838,"line":1028},[939,1970,945],{"class":944},[939,1972,949],{"class":948},[939,1974,1975],{"class":952},"\"\u002Fdocuments\u002Fmeta-only\"",[939,1977,956],{"class":948},[939,1979,1980,1982,1984,1987,1990,1992],{"class":838,"line":1043},[939,1981,963],{"class":962},[939,1983,966],{"class":962},[939,1985,1986],{"class":944}," document_meta",[939,1988,1989],{"class":948},"(metadata: Annotated[Metadata, Form()]) -> dict[",[939,1991,976],{"class":975},[939,1993,979],{"class":948},[939,1995,1996],{"class":838,"line":1052},[939,1997,1998],{"class":985},"    # Its fields are read FLAT from the form, as long as it is the ONLY body parameter.\n",[939,2000,2001,2003],{"class":838,"line":1067},[939,2002,1046],{"class":962},[939,2004,2005],{"class":948}," metadata.model_dump()\n",[939,2007,2008],{"class":838,"line":1080},[939,2009,1143],{"emptyLinePlaceholder":1142},[939,2011,2012],{"class":838,"line":1094},[939,2013,1143],{"emptyLinePlaceholder":1142},[939,2015,2016,2018,2020,2023],{"class":838,"line":1109},[939,2017,945],{"class":944},[939,2019,949],{"class":948},[939,2021,2022],{"class":952},"\"\u002Fdocuments\u002Fwith-file\"",[939,2024,956],{"class":948},[939,2026,2027,2029,2031,2034],{"class":838,"line":1133},[939,2028,963],{"class":962},[939,2030,966],{"class":962},[939,2032,2033],{"class":944}," document_with_file",[939,2035,1738],{"class":948},[939,2037,2038],{"class":838,"line":1139},[939,2039,2040],{"class":948},"    metadata: Annotated[Metadata, Form()],\n",[939,2042,2043],{"class":838,"line":1146},[939,2044,2045],{"class":948},"    file: UploadFile,\n",[939,2047,2048,2050,2052],{"class":838,"line":1151},[939,2049,1820],{"class":948},[939,2051,976],{"class":975},[939,2053,979],{"class":948},[939,2055,2056],{"class":838,"line":1163},[939,2057,2058],{"class":985},"    # Adding a second body parameter makes FastAPI embed the model under its own name.\n",[939,2060,2061,2063,2065,2067,2069],{"class":838,"line":1185},[939,2062,1031],{"class":948},[939,2064,995],{"class":962},[939,2066,998],{"class":962},[939,2068,1002],{"class":1001},[939,2070,1040],{"class":948},[939,2072,2073,2075,2077,2080,2083,2085,2087,2089],{"class":838,"line":1191},[939,2074,1046],{"class":962},[939,2076,1196],{"class":948},[939,2078,2079],{"class":952},"\"metadata\"",[939,2081,2082],{"class":948},": metadata.model_dump(), ",[939,2084,1199],{"class":952},[939,2086,1058],{"class":948},[939,2088,1088],{"class":975},[939,2090,1501],{"class":948},[590,2092,2093,2094,1322,2096,2099],{},"The same flat form data (",[603,2095,732],{},[603,2097,2098],{},"tags",") posted to both:",[930,2101,2104],{"className":2102,"code":2103,"language":749,"meta":935},[1235],"  {\n    \"request\": \"POST \u002Fdocuments\u002Fmeta-only  (form model alone, flat fields)\",\n    \"status\": 200,\n    \"response\": {\n      \"title\": \"Q2 report\",\n      \"tags\": [\n        \"finance\",\n        \"internal\"\n      ]\n    }\n  },\n  {\n    \"request\": \"POST \u002Fdocuments\u002Fwith-file  (form model + file, same flat fields)\",\n    \"status\": 422,\n    \"response\": {\n      \"detail\": [\n        {\n          \"type\": \"missing\",\n          \"loc\": [\n            \"body\",\n            \"metadata\"\n          ],\n          \"msg\": \"Field required\",\n          \"input\": null\n        }\n      ]\n    }\n  },\n",[603,2105,2103],{"__ignoreMap":935},[590,2107,2108,2109,2112],{},"That is the multiple-body-parameter rule again, in its least obvious costume. The file counts as a body parameter, so the model gets embedded and FastAPI now looks for a form field literally named ",[603,2110,2111],{},"metadata",". Browsers do not post nested form fields, so the endpoint is effectively unusable as written.",[590,2114,2115],{},"The version that works declares the fields flat:",[930,2117,2119],{"className":932,"code":2118,"language":934,"meta":935,"style":935},"@app.post(\"\u002Fdocuments\u002Fflat\")\nasync def document_flat(\n    title: Annotated[str, Form()],\n    file: UploadFile,\n    tags: Annotated[list[str], Form()] = [],\n) -> dict[str, Any]:\n    body = await file.read()\n    return {\"title\": title, \"tags\": tags, \"filename\": file.filename, \"size\": len(body)}\n",[603,2120,2121,2132,2143,2153,2157,2172,2180,2192],{"__ignoreMap":935},[939,2122,2123,2125,2127,2130],{"class":838,"line":941},[939,2124,945],{"class":944},[939,2126,949],{"class":948},[939,2128,2129],{"class":952},"\"\u002Fdocuments\u002Fflat\"",[939,2131,956],{"class":948},[939,2133,2134,2136,2138,2141],{"class":838,"line":959},[939,2135,963],{"class":962},[939,2137,966],{"class":962},[939,2139,2140],{"class":944}," document_flat",[939,2142,1738],{"class":948},[939,2144,2145,2148,2150],{"class":838,"line":982},[939,2146,2147],{"class":948},"    title: Annotated[",[939,2149,976],{"class":975},[939,2151,2152],{"class":948},", Form()],\n",[939,2154,2155],{"class":838,"line":989},[939,2156,2045],{"class":948},[939,2158,2159,2162,2164,2167,2169],{"class":838,"line":1012},[939,2160,2161],{"class":948},"    tags: Annotated[list[",[939,2163,976],{"class":975},[939,2165,2166],{"class":948},"], Form()] ",[939,2168,995],{"class":962},[939,2170,2171],{"class":948}," [],\n",[939,2173,2174,2176,2178],{"class":838,"line":1028},[939,2175,1820],{"class":948},[939,2177,976],{"class":975},[939,2179,979],{"class":948},[939,2181,2182,2184,2186,2188,2190],{"class":838,"line":1043},[939,2183,1031],{"class":948},[939,2185,995],{"class":962},[939,2187,998],{"class":962},[939,2189,1002],{"class":1001},[939,2191,1040],{"class":948},[939,2193,2194,2196,2198,2201,2204,2207,2210,2212,2214,2216,2218,2220,2222,2224],{"class":838,"line":1052},[939,2195,1046],{"class":962},[939,2197,1196],{"class":948},[939,2199,2200],{"class":952},"\"title\"",[939,2202,2203],{"class":948},": title, ",[939,2205,2206],{"class":952},"\"tags\"",[939,2208,2209],{"class":948},": tags, ",[939,2211,1876],{"class":952},[939,2213,1058],{"class":948},[939,2215,1061],{"class":1001},[939,2217,1492],{"class":948},[939,2219,1199],{"class":952},[939,2221,1058],{"class":948},[939,2223,1088],{"class":975},[939,2225,1501],{"class":948},[930,2227,2230],{"className":2228,"code":2229,"language":749,"meta":935},[1235],"  {\n    \"request\": \"POST \u002Fdocuments\u002Fflat  (flat Form fields + file)\",\n    \"status\": 200,\n    \"response\": {\n      \"title\": \"Q2 report\",\n      \"tags\": [\n        \"finance\",\n        \"internal\"\n      ],\n      \"filename\": \"q2.pdf\",\n      \"size\": 13\n    }\n  },\n",[603,2231,2229],{"__ignoreMap":935},[590,2233,2234,2235,2238],{},"Note also that a repeated form key collects into a ",[603,2236,2237],{},"list[str]",", exactly as a repeated query key does.",[898,2240,2242],{"id":2241},"why-json-and-form-cannot-share-an-endpoint","Why JSON and Form Cannot Share an Endpoint",[590,2244,2245,2246,2248,2249,681,2251,2254,2255,2258],{},"The reason is HTTP, not FastAPI: a request has one body and one ",[603,2247,911],{},". Parsing form fields requires ",[603,2250,708],{},[603,2252,2253],{},"application\u002Fx-www-form-urlencoded","; parsing a JSON body requires ",[603,2256,2257],{},"application\u002Fjson",". No single request is both.",[590,2260,2261],{},"What makes this confusing is that FastAPI does not stop you declaring it. This endpoint imports and starts cleanly:",[930,2263,2265],{"className":932,"code":2264,"language":934,"meta":935,"style":935},"@app.post(\"\u002Fmixed\u002F\")\nasync def mixed(metadata: Metadata, note: Annotated[str, Form()]) -> dict[str, Any]:\n    # A JSON body parameter alongside a Form field.\n    return {\"metadata\": metadata.model_dump(), \"note\": note}\n",[603,2266,2267,2278,2299,2304],{"__ignoreMap":935},[939,2268,2269,2271,2273,2276],{"class":838,"line":941},[939,2270,945],{"class":944},[939,2272,949],{"class":948},[939,2274,2275],{"class":952},"\"\u002Fmixed\u002F\"",[939,2277,956],{"class":948},[939,2279,2280,2282,2284,2287,2290,2292,2295,2297],{"class":838,"line":959},[939,2281,963],{"class":962},[939,2283,966],{"class":962},[939,2285,2286],{"class":944}," mixed",[939,2288,2289],{"class":948},"(metadata: Metadata, note: Annotated[",[939,2291,976],{"class":975},[939,2293,2294],{"class":948},", Form()]) -> dict[",[939,2296,976],{"class":975},[939,2298,979],{"class":948},[939,2300,2301],{"class":838,"line":982},[939,2302,2303],{"class":985},"    # A JSON body parameter alongside a Form field.\n",[939,2305,2306,2308,2310,2312,2314,2317],{"class":838,"line":989},[939,2307,1046],{"class":962},[939,2309,1196],{"class":948},[939,2311,2079],{"class":952},[939,2313,2082],{"class":948},[939,2315,2316],{"class":952},"\"note\"",[939,2318,2319],{"class":948},": note}\n",[590,2321,2322],{},"Posted both ways, it fails both ways:",[930,2324,2327],{"className":2325,"code":2326,"language":749,"meta":935},[1235],"  {\n    \"request\": \"POST \u002Fmixed\u002F  (multipart form fields)\",\n    \"status\": 422,\n    \"response\": {\n      \"detail\": [\n        {\n          \"type\": \"missing\",\n          \"loc\": [\n            \"body\",\n            \"metadata\"\n          ],\n          \"msg\": \"Field required\",\n          \"input\": null\n        }\n      ]\n    }\n  },\n  {\n    \"request\": \"POST \u002Fmixed\u002F  (application\u002Fjson body)\",\n    \"status\": 422,\n    \"response\": {\n      \"detail\": [\n        {\n          \"type\": \"missing\",\n          \"loc\": [\n            \"body\",\n            \"metadata\"\n          ],\n          \"msg\": \"Field required\",\n          \"input\": null\n        },\n        {\n          \"type\": \"missing\",\n          \"loc\": [\n            \"body\",\n            \"note\"\n          ],\n          \"msg\": \"Field required\",\n          \"input\": null\n        }\n      ]\n    }\n  }\n",[603,2328,2326],{"__ignoreMap":935},[590,2330,2331,2332,2334,2335,2337],{},"There is no request that produces a 200. The presence of a ",[603,2333,623],{}," parameter puts the whole endpoint into form-parsing mode, so the JSON body is never decoded — and a form post cannot supply a nested ",[603,2336,2111],{}," object.",[590,2339,2340,2341,2344,2345,2348,2349,2352,2353,2356,2357,1058,2360,2363,2364,2367],{},"Two ways out. ",[593,2342,2343],{},"Send the JSON as a form field"," and parse it yourself: declare ",[603,2346,2347],{},"metadata: Annotated[str, Form()]"," and call ",[603,2350,2351],{},"Metadata.model_validate_json(metadata)",", catching ",[603,2354,2355],{},"ValidationError"," and re-raising as a 422. Browsers can do this, and it keeps one round trip. ",[593,2358,2359],{},"Or split the endpoint",[603,2361,2362],{},"POST \u002Fdocuments\u002F"," takes JSON and returns an id, then ",[603,2365,2366],{},"PUT \u002Fdocuments\u002F{id}\u002Fcontent"," takes the multipart upload. That is more requests but a cleaner contract, and it lets the upload go direct to object storage with a pre-signed URL later without changing the metadata API.",[898,2369,2371],{"id":2370},"verification","Verification",[590,2373,2374,2375,2378,2379,2382],{},"Test uploads with ",[603,2376,2377],{},"TestClient",", which accepts the same ",[603,2380,2381],{},"files="," argument as httpx:",[930,2384,2386],{"className":932,"code":2385,"language":934,"meta":935,"style":935},"def test_rejects_oversized_upload():\n    client = TestClient(app)\n    response = client.post(\n        \"\u002Favatar\u002Fchecked\",\n        files={\"file\": (\"big.png\", b\"\\x89PNG\\r\\n\\x1a\\n\" + b\"\\x00\" * 4096, \"image\u002Fpng\")},\n    )\n    assert response.status_code == 413\n\n\ndef test_form_and_file_validate_together():\n    client = TestClient(app)\n    response = client.post(\"\u002Fprofiles\u002F\", data={\"display_name\": \"A\", \"age\": \"9\"})\n    locs = {tuple(e[\"loc\"]) for e in response.json()[\"detail\"]}\n    assert (\"body\", \"avatar\") in locs\n",[603,2387,2388,2399,2409,2419,2426,2486,2491,2505,2509,2513,2522,2530,2571,2610],{"__ignoreMap":935},[939,2389,2390,2393,2396],{"class":838,"line":941},[939,2391,2392],{"class":962},"def",[939,2394,2395],{"class":944}," test_rejects_oversized_upload",[939,2397,2398],{"class":948},"():\n",[939,2400,2401,2404,2406],{"class":838,"line":959},[939,2402,2403],{"class":948},"    client ",[939,2405,995],{"class":962},[939,2407,2408],{"class":948}," TestClient(app)\n",[939,2410,2411,2414,2416],{"class":838,"line":982},[939,2412,2413],{"class":948},"    response ",[939,2415,995],{"class":962},[939,2417,2418],{"class":948}," client.post(\n",[939,2420,2421,2424],{"class":838,"line":989},[939,2422,2423],{"class":952},"        \"\u002Favatar\u002Fchecked\"",[939,2425,1130],{"class":948},[939,2427,2428,2431,2433,2435,2438,2441,2444,2446,2449,2451,2454,2457,2460,2462,2465,2467,2469,2472,2474,2476,2479,2481,2483],{"class":838,"line":1012},[939,2429,2430],{"class":1001},"        files",[939,2432,995],{"class":962},[939,2434,1405],{"class":948},[939,2436,2437],{"class":952},"\"file\"",[939,2439,2440],{"class":948},": (",[939,2442,2443],{"class":952},"\"big.png\"",[939,2445,1322],{"class":948},[939,2447,2448],{"class":962},"b",[939,2450,1416],{"class":952},[939,2452,2453],{"class":962},"\\x89",[939,2455,2456],{"class":952},"PNG",[939,2458,2459],{"class":962},"\\r\\n\\x1a\\n",[939,2461,1416],{"class":952},[939,2463,2464],{"class":962}," +",[939,2466,1671],{"class":962},[939,2468,1416],{"class":952},[939,2470,2471],{"class":962},"\\x00",[939,2473,1416],{"class":952},[939,2475,1601],{"class":962},[939,2477,2478],{"class":975}," 4096",[939,2480,1322],{"class":948},[939,2482,1319],{"class":952},[939,2484,2485],{"class":948},")},\n",[939,2487,2488],{"class":838,"line":1028},[939,2489,2490],{"class":948},"    )\n",[939,2492,2493,2496,2499,2502],{"class":838,"line":1043},[939,2494,2495],{"class":962},"    assert",[939,2497,2498],{"class":948}," response.status_code ",[939,2500,2501],{"class":962},"==",[939,2503,2504],{"class":975}," 413\n",[939,2506,2507],{"class":838,"line":1052},[939,2508,1143],{"emptyLinePlaceholder":1142},[939,2510,2511],{"class":838,"line":1067},[939,2512,1143],{"emptyLinePlaceholder":1142},[939,2514,2515,2517,2520],{"class":838,"line":1080},[939,2516,2392],{"class":962},[939,2518,2519],{"class":944}," test_form_and_file_validate_together",[939,2521,2398],{"class":948},[939,2523,2524,2526,2528],{"class":838,"line":1094},[939,2525,2403],{"class":948},[939,2527,995],{"class":962},[939,2529,2408],{"class":948},[939,2531,2532,2534,2536,2539,2541,2543,2546,2548,2550,2553,2555,2558,2560,2563,2565,2568],{"class":838,"line":1109},[939,2533,2413],{"class":948},[939,2535,995],{"class":962},[939,2537,2538],{"class":948}," client.post(",[939,2540,1724],{"class":952},[939,2542,1322],{"class":948},[939,2544,2545],{"class":1001},"data",[939,2547,995],{"class":962},[939,2549,1405],{"class":948},[939,2551,2552],{"class":952},"\"display_name\"",[939,2554,1058],{"class":948},[939,2556,2557],{"class":952},"\"A\"",[939,2559,1322],{"class":948},[939,2561,2562],{"class":952},"\"age\"",[939,2564,1058],{"class":948},[939,2566,2567],{"class":952},"\"9\"",[939,2569,2570],{"class":948},"})\n",[939,2572,2573,2576,2578,2580,2583,2586,2589,2592,2595,2598,2601,2604,2607],{"class":838,"line":1133},[939,2574,2575],{"class":948},"    locs ",[939,2577,995],{"class":962},[939,2579,1196],{"class":948},[939,2581,2582],{"class":975},"tuple",[939,2584,2585],{"class":948},"(e[",[939,2587,2588],{"class":952},"\"loc\"",[939,2590,2591],{"class":948},"]) ",[939,2593,2594],{"class":962},"for",[939,2596,2597],{"class":948}," e ",[939,2599,2600],{"class":962},"in",[939,2602,2603],{"class":948}," response.json()[",[939,2605,2606],{"class":952},"\"detail\"",[939,2608,2609],{"class":948},"]}\n",[939,2611,2612,2614,2617,2620,2622,2625,2628,2630],{"class":838,"line":1139},[939,2613,2495],{"class":962},[939,2615,2616],{"class":948}," (",[939,2618,2619],{"class":952},"\"body\"",[939,2621,1322],{"class":948},[939,2623,2624],{"class":952},"\"avatar\"",[939,2626,2627],{"class":948},") ",[939,2629,2600],{"class":962},[939,2631,2632],{"class":948}," locs\n",[590,2634,2635,2636,639],{},"In production, the signal to watch is the ratio of 413 and 415 responses to successful uploads on the same route. A sudden 413 spike usually means a client shipped a new capture resolution, not that anyone is attacking you. Wiring that up is covered in ",[662,2637,2639],{"href":2638},"\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002Fprometheus-metrics-for-fastapi\u002F","Prometheus metrics for FastAPI",[898,2641,2643],{"id":2642},"trade-offs-and-when-not-to","Trade-offs and When Not To",[590,2645,2646,2649],{},[593,2647,2648],{},"Uploading through your API at all."," For anything above a few megabytes, pre-signed URLs to object storage are better: the bytes never touch your application, so upload traffic stops competing with request handling. The API then validates only the metadata and the resulting object. Accept uploads directly when you must inspect or transform the content synchronously, or when the client cannot be trusted with a storage credential.",[590,2651,2652,2655,2656,2660],{},[593,2653,2654],{},"Reading the file in the request handler."," Virus scanning, transcoding and thumbnailing are not request-time work. Persist the raw bytes, return 202, and process out of band — see ",[662,2657,2659],{"href":2658},"\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002Ffastapi-backgroundtasks-vs-celery-vs-arq\u002F","FastAPI BackgroundTasks vs Celery vs arq"," for choosing the mechanism.",[590,2662,2663,2666,2667,2669,2670,2673,2674,639],{},[593,2664,2665],{},"Blocking file I\u002FO in an async handler."," ",[603,2668,1536],{}," is fine, but a subsequent ",[603,2671,2672],{},"open(path, \"wb\").write(...)"," or a Pillow call is synchronous CPU and I\u002FO on the event loop. Push it to a threadpool; the reasoning is in ",[662,2675,2677],{"href":2676},"\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffixing-blocking-calls-in-async-routes\u002F","fixing blocking calls in async routes",[590,2679,2680,2685,2686,2689],{},[593,2681,2682,2683,639],{},"Trusting ",[603,2684,609],{}," It is client-controlled and may contain ",[603,2687,2688],{},"..\u002F"," or a null byte. Never join it onto a path. Generate your own identifier and keep the original name as metadata only.",[898,2691,2693],{"id":2692},"faq","FAQ",[590,2695,2696,2705,2706,2708,2709,2711,2712,610,2714,2716,2717,2719],{},[593,2697,2698,2699,2701,2702,2704],{},"Should I declare an upload as ",[603,2700,605],{}," or as ",[603,2703,925],{},"?","\nUse ",[603,2707,605],{}," unless the payload is guaranteed small. ",[603,2710,605],{}," is a spooled file that stays on disk past a threshold and exposes ",[603,2713,609],{},[603,2715,613],{},", while ",[603,2718,617],{}," reads the entire upload into memory before your handler runs, so a large upload is an allocation you cannot refuse.",[590,2721,2722,2725,2726,2728,2729,681,2731,2733,2734,2736],{},[593,2723,2724],{},"Why can I not accept a JSON body and a form field in the same endpoint?","\nA request has one body and one ",[603,2727,911],{},". Form parsing needs ",[603,2730,708],{},[603,2732,2253],{},", JSON parsing needs ",[603,2735,2257],{},", and no request satisfies both. FastAPI will not error at import time; the endpoint simply returns 422 for every request because one of the two parameters can never be populated.",[590,2738,2739,2748,2749,2751],{},[593,2740,2741,2742,2744,2745,2747],{},"Is the ",[603,2743,613],{}," on an ",[603,2746,605],{}," trustworthy?","\nNo. It is the ",[603,2750,911],{}," the client wrote into the multipart part header, so it is entirely client-controlled. Use it as a cheap first filter, then confirm by inspecting the actual bytes with a magic-number check before storing or processing the file.",[590,2753,2754,2757,2758,2760],{},[593,2755,2756],{},"How do I enforce a maximum upload size?","\nEnforce it at the proxy or ASGI server for a hard ceiling, and read the file in chunks in the handler so you can abort with a 413 once the running total is exceeded. Trusting the ",[603,2759,650],{}," header alone is not enough, because a chunked request need not send one.",[590,2762,2763,2766,2767,2769,2770,2772],{},[593,2764,2765],{},"Why did my Pydantic form model stop working when I added a file parameter?","\nA ",[603,2768,623],{},"-bound model is read flat from the form only while it is the sole body parameter. Adding an ",[603,2771,605],{}," makes it the second body parameter, so FastAPI embeds the model under its parameter name and then reports it as missing, exactly as it does with multiple JSON body parameters.",[898,2774,2776],{"id":2775},"related-reading","Related Reading",[597,2778,2779,2787,2796,2806],{},[600,2780,2781,2666,2784,2786],{},[593,2782,2783],{},"Up to the guide:",[662,2785,665],{"href":664}," for the 422 anatomy these transcripts follow.",[600,2788,2789,2666,2792,2795],{},[593,2790,2791],{},"The JSON equivalent:",[662,2793,2794],{"href":669},"Query, Path and Body Parameter Validation",", where the multiple-body-parameter rule is spelled out in full.",[600,2797,2798,2666,2801,2805],{},[593,2799,2800],{},"Sending files back:",[662,2802,2804],{"href":2803},"\u002Fcore-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fstreaming-and-file-responses\u002F","Streaming and File Responses"," for the outbound direction.",[600,2807,2808,2666,2811,2815],{},[593,2809,2810],{},"Testing the multipart path:",[662,2812,2814],{"href":2813},"\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftestclient-vs-httpx-asyncclient\u002F","TestClient vs httpx AsyncClient"," for the transport used to produce this page's output.",[2817,2818,2819],"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 .sFeEa, html code.shiki .sFeEa{--shiki-default:#66707B}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);}",{"title":935,"searchDepth":959,"depth":959,"links":2821},[2822,2823,2824,2826,2827,2828,2829,2830,2831,2832,2833],{"id":689,"depth":982,"text":690},{"id":900,"depth":959,"text":901},{"id":919,"depth":959,"text":2825},"UploadFile versus bytes",{"id":1284,"depth":959,"text":1285},{"id":1701,"depth":959,"text":1702},{"id":1907,"depth":959,"text":1908},{"id":2241,"depth":959,"text":2242},{"id":2370,"depth":959,"text":2371},{"id":2642,"depth":959,"text":2643},{"id":2692,"depth":959,"text":2693},{"id":2775,"depth":959,"text":2776},"2026-07-20","UploadFile versus bytes, Form fields beside files, real size and content-type limits, and why a JSON body and form fields cannot share one FastAPI endpoint.","md",[2838,2841,2843,2846,2848],{"q":2839,"a":2840},"Should I declare an upload as UploadFile or as bytes?","Use UploadFile unless the payload is guaranteed small. UploadFile is a spooled file that stays on disk past a threshold and exposes filename and content_type, while bytes = File() reads the entire upload into memory before your handler runs, so a large upload is an allocation you cannot refuse.",{"q":2724,"a":2842},"A request has one body and one Content-Type. Form parsing needs multipart\u002Fform-data or application\u002Fx-www-form-urlencoded, JSON parsing needs application\u002Fjson, and no request satisfies both. FastAPI will not error at import time; the endpoint simply returns 422 for every request because one of the two parameters can never be populated.",{"q":2844,"a":2845},"Is the content_type on an UploadFile trustworthy?","No. It is the Content-Type the client wrote into the multipart part header, so it is entirely client-controlled. Use it as a cheap first filter, then confirm by inspecting the actual bytes with a magic-number check before storing or processing the file.",{"q":2756,"a":2847},"Enforce it at the proxy or ASGI server for a hard ceiling, and read the file in chunks in the handler so you can abort with a 413 once the running total is exceeded. Trusting the Content-Length header alone is not enough, because a chunked request need not send one.",{"q":2765,"a":2849},"A Form-bound model is read flat from the form only while it is the sole body parameter. Adding an UploadFile makes it the second body parameter, so FastAPI embeds the model under its parameter name and then reports it as missing, exactly as it does with multiple JSON body parameters.",null,{"slug":2852,"breadcrumb":2853},"validating-file-uploads-and-forms",[2854,2857,2860,2861],{"label":2855,"path":2856},"Home","\u002F",{"label":2858,"path":2859},"Advanced Pydantic Validation & Serialization","\u002Fadvanced-pydantic-validation-serialization\u002F",{"label":665,"path":664},{"label":2862,"path":2863},"Validating File Uploads and Forms","\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fvalidating-file-uploads-and-forms\u002F",{"title":169,"description":2835},"article","vpYzcUO4E7TtpwiXa4Ln9ShcaTiw4dHnBtPr4X69x3I",[2850,2850],1784588202620]