[{"data":1,"prerenderedAt":1880},["ShallowReactive",2],{"nav":3,"page-\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fdiscriminated-unions-in-openapi\u002F":580,"surround-\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fdiscriminated-unions-in-openapi\u002F":1879},[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":55,"body":582,"dateModified":1848,"datePublished":1848,"description":1849,"extension":1850,"faq":1851,"howto":1862,"meta":1863,"navigation":910,"path":56,"seo":1876,"stem":57,"type":1877,"__hash__":1878},"content\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fdiscriminated-unions-in-openapi\u002Findex.md",{"type":583,"value":584,"toc":1834},"minimark",[585,589,596,635,644,649,656,659,799,803,806,824,834,858,861,865,871,1296,1299,1304,1311,1318,1322,1328,1335,1339,1342,1348,1351,1355,1361,1382,1386,1389,1505,1515,1663,1666,1670,1679,1698,1704,1720,1726,1734,1738,1752,1758,1770,1779,1792,1796,1830],[586,587,55],"h1",{"id":588},"discriminated-unions-in-openapi-with-pydantic",[590,591,592],"p",{},[593,594,595],"strong",{},"Key takeaways:",[597,598,599,607,618,621,628],"ul",{},[600,601,602,606],"li",{},[603,604,605],"code",{},"Field(discriminator=\"method\")"," makes Pydantic read one literal field and validate a single variant.",[600,608,609,610,613,614,617],{},"The generated schema becomes ",[603,611,612],{},"oneOf"," plus a ",[603,615,616],{},"discriminator"," mapping, which client generators can narrow on.",[600,619,620],{},"An untagged union reports failures for every member; a tagged one reports only the variant you sent.",[600,622,623,624,627],{},"An unknown tag produces one ",[603,625,626],{},"union_tag_invalid"," error listing the tags that were acceptable.",[600,629,630,631,634],{},"The discriminator must be a ",[603,632,633],{},"Literal"," field, spelled identically on every member, and never optional.",[590,636,637,638,643],{},"This guide extends ",[639,640,642],"a",{"href":641},"\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002F","JSON Schema Customization",", which covers shaping the generated document; here the shape change also transforms the error responses your API returns.",[645,646,648],"h2",{"id":647},"the-problem-this-solves","The Problem This Solves",[590,650,651,652,655],{},"A payments endpoint accepts a card, a bank transfer or a wallet. The natural annotation is ",[603,653,654],{},"Union[CardPayment, BankTransferPayment, WalletPayment]",", and it works — right up until someone sends a card with a two-digit number. Then they get a 422 containing eight errors, six of which complain about missing IBANs and wallet tokens for payment methods they were never attempting.",[590,657,658],{},"A support engineer reading that body cannot tell what went wrong. Neither can the client developer, and neither can an error-tracking dashboard trying to group failures by cause.",[660,661,667,671,675,682,687,698,703,710,712,716,720,725,728,731,734,737,741,743,745,750,755,760,763,765,769,772,775,778,783,786,789,794],"svg",{"viewBox":662,"role":663,"ariaLabel":664,"xmlns":665,"style":666},"0 0 720 310","img","An untagged union validating against every member versus a tagged union validating against one","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;max-width:720px;margin:2rem 0",[668,669,670],"title",{},"Untagged versus tagged union validation",[672,673,674],"desc",{},"On the left an untagged union validates the payload against all three members and returns eight errors. On the right a discriminated union reads the method tag, validates only the card variant, and returns two errors.",[676,677,681],"text",{"x":678,"y":679,"style":680},"180","28","text-anchor:middle;fill:currentColor;font:700 13px sans-serif","Union[...] — anyOf",[676,683,686],{"x":684,"y":679,"style":685},"540","text-anchor:middle;fill:#00796B;font:700 13px sans-serif","discriminator=\"method\" — oneOf",[688,689],"rect",{"x":690,"y":691,"width":692,"height":693,"rx":694,"fill":695,"stroke":696,"strokeWidth":697},"110","44","140","36","8","none","currentColor","1.5",[676,699,702],{"x":678,"y":700,"style":701},"67","text-anchor:middle;fill:currentColor;font:400 12px sans-serif","body.payment",[704,705],"line",{"x1":706,"y1":707,"x2":708,"y2":709,"stroke":696,"strokeWidth":697},"150","80","65","108",[704,711],{"x1":678,"y1":707,"x2":678,"y2":709,"stroke":696,"strokeWidth":697},[704,713],{"x1":714,"y1":707,"x2":715,"y2":709,"stroke":696,"strokeWidth":697},"210","295",[688,717],{"x":718,"y":690,"width":719,"height":693,"rx":694,"fill":695,"stroke":696,"strokeWidth":697},"15","100",[676,721,724],{"x":708,"y":722,"style":723},"133","text-anchor:middle;fill:currentColor;font:400 11px sans-serif","Card",[688,726],{"x":727,"y":690,"width":719,"height":693,"rx":694,"fill":695,"stroke":696,"strokeWidth":697},"130",[676,729,730],{"x":678,"y":722,"style":723},"BankTransfer",[688,732],{"x":733,"y":690,"width":719,"height":693,"rx":694,"fill":695,"stroke":696,"strokeWidth":697},"245",[676,735,736],{"x":715,"y":722,"style":723},"Wallet",[704,738],{"x1":708,"y1":739,"x2":706,"y2":740,"stroke":696,"strokeWidth":697},"146","228",[704,742],{"x1":678,"y1":739,"x2":678,"y2":740,"stroke":696,"strokeWidth":697},[704,744],{"x1":715,"y1":739,"x2":714,"y2":740,"stroke":696,"strokeWidth":697},[688,746],{"x":747,"y":748,"width":749,"height":691,"rx":694,"fill":695,"stroke":696,"strokeWidth":697},"60","230","240",[676,751,754],{"x":678,"y":752,"style":753},"258","text-anchor:middle;fill:currentColor;font:600 12px sans-serif","422 with 8 errors",[688,756],{"x":757,"y":691,"width":692,"height":693,"rx":694,"fill":695,"stroke":758,"strokeWidth":759},"470","#00796B","2",[676,761,702],{"x":684,"y":700,"style":762},"text-anchor:middle;fill:#00796B;font:400 12px sans-serif",[704,764],{"x1":684,"y1":707,"x2":684,"y2":709,"stroke":758,"strokeWidth":697},[688,766],{"x":767,"y":690,"width":768,"height":693,"rx":694,"fill":695,"stroke":758,"strokeWidth":759},"445","190",[676,770,771],{"x":684,"y":722,"style":762},"read method — \"card\"",[704,773],{"x1":684,"y1":739,"x2":684,"y2":774,"stroke":758,"strokeWidth":697},"168",[688,776],{"x":757,"y":777,"width":692,"height":693,"rx":694,"fill":695,"stroke":758,"strokeWidth":759},"170",[676,779,782],{"x":684,"y":780,"style":781},"193","text-anchor:middle;fill:#00796B;font:400 11px sans-serif","CardPayment only",[704,784],{"x1":684,"y1":785,"x2":684,"y2":740,"stroke":758,"strokeWidth":697},"206",[688,787],{"x":788,"y":748,"width":749,"height":691,"rx":694,"fill":695,"stroke":758,"strokeWidth":759},"430",[676,790,793],{"x":791,"y":752,"style":792},"550","text-anchor:middle;fill:#00796B;font:600 12px sans-serif","422 with 2 errors",[676,795,798],{"x":796,"y":797,"style":701},"360","300","Same payload, same models — only the annotation differs.",[645,800,802],{"id":801},"why-it-happens","Why It Happens",[590,804,805],{},"Pydantic's smart union mode tries each member in turn. When every member fails, it has no basis for deciding which failure the caller cares about, so it returns all of them, tagged with the member's class name in the error location. The eight-error body is not a bug; it is the only honest answer to \"this matched nothing\" when nothing indicated what it was supposed to match.",[590,807,808,809,811,812,815,816,820,821,823],{},"Adding ",[603,810,605],{}," changes the question. Pydantic builds a tagged-union validator that reads the ",[603,813,814],{},"method"," key from the raw input ",[817,818,819],"em",{},"before"," any member validation runs, looks it up in a mapping built from the ",[603,822,633],{}," annotations, and dispatches to exactly one member. If the tag is missing or unrecognised, validation stops there with a single error about the tag itself.",[590,825,826,827,830,831,833],{},"That short-circuit is why the error quality improves so much: once the tag says ",[603,828,829],{},"card",", every subsequent error is unambiguously about card payments, and the error location says ",[603,832,829],{}," rather than naming a class the caller has never heard of.",[590,835,836,837,840,841,844,845,613,847,849,850,853,854,857],{},"The schema changes to match. An untagged union generates ",[603,838,839],{},"anyOf"," with a list of ",[603,842,843],{},"$ref","s — \"any of these will do\", which tells a code generator nothing about how to pick. A discriminated union generates ",[603,846,612],{},[603,848,616],{}," object containing ",[603,851,852],{},"propertyName"," and an explicit ",[603,855,856],{},"mapping"," from each tag value to its schema reference. That mapping is a machine-readable dispatch table, and generators for TypeScript, Kotlin and Go all consume it to emit narrowable tagged types.",[590,859,860],{},"There is a performance dimension too, though it is the least interesting one: dispatching on a dictionary lookup instead of attempting three full model validations is strictly less work. The correctness and ergonomics are the reasons to do it.",[645,862,864],{"id":863},"the-fix","The Fix",[590,866,867,868,870],{},"Every member gets a ",[603,869,633],{}," field with the same name and a distinct value. The union is annotated once and reused.",[872,873,878],"pre",{"className":874,"code":875,"language":876,"meta":877,"style":877},"language-python shiki shiki-themes github-light-high-contrast","\"\"\"Compare the 422 from a tagged (discriminated) union with the one from a plain union.\"\"\"\nfrom typing import Annotated, Literal, Union\n\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel, Field\n\n\nclass CardPayment(BaseModel):\n    method: Literal[\"card\"]\n    card_number: str = Field(min_length=12, max_length=19)\n    cvc: str = Field(min_length=3, max_length=4)\n\n\nclass BankTransferPayment(BaseModel):\n    method: Literal[\"bank_transfer\"]\n    iban: str = Field(min_length=15)\n    reference: str\n\n\nclass WalletPayment(BaseModel):\n    method: Literal[\"wallet\"]\n    wallet_id: str\n    device_token: str\n\n\nTagged = Annotated[\n    Union[CardPayment, BankTransferPayment, WalletPayment],\n    Field(discriminator=\"method\"),\n]\nUntagged = Union[CardPayment, BankTransferPayment, WalletPayment]\n\n\nclass TaggedEnvelope(BaseModel):\n    amount_cents: int\n    payment: Tagged\n\n\nclass UntaggedEnvelope(BaseModel):\n    amount_cents: int\n    payment: Untagged\n","python","",[603,879,880,888,905,912,925,938,943,948,968,980,1018,1048,1053,1058,1072,1082,1102,1111,1116,1121,1135,1145,1153,1161,1166,1171,1182,1188,1204,1209,1220,1225,1230,1244,1253,1259,1264,1269,1283,1290],{"__ignoreMap":877},[881,882,884],"span",{"class":704,"line":883},1,[881,885,887],{"class":886},"sYEJz","\"\"\"Compare the 422 from a tagged (discriminated) union with the one from a plain union.\"\"\"\n",[881,889,891,895,899,902],{"class":704,"line":890},2,[881,892,894],{"class":893},"sTJeM","from",[881,896,898],{"class":897},"sigWx"," typing ",[881,900,901],{"class":893},"import",[881,903,904],{"class":897}," Annotated, Literal, Union\n",[881,906,908],{"class":704,"line":907},3,[881,909,911],{"emptyLinePlaceholder":910},true,"\n",[881,913,915,917,920,922],{"class":704,"line":914},4,[881,916,894],{"class":893},[881,918,919],{"class":897}," fastapi ",[881,921,901],{"class":893},[881,923,924],{"class":897}," FastAPI\n",[881,926,928,930,933,935],{"class":704,"line":927},5,[881,929,894],{"class":893},[881,931,932],{"class":897}," pydantic ",[881,934,901],{"class":893},[881,936,937],{"class":897}," BaseModel, Field\n",[881,939,941],{"class":704,"line":940},6,[881,942,911],{"emptyLinePlaceholder":910},[881,944,946],{"class":704,"line":945},7,[881,947,911],{"emptyLinePlaceholder":910},[881,949,951,954,958,961,965],{"class":704,"line":950},8,[881,952,953],{"class":893},"class",[881,955,957],{"class":956},"sV4o_"," CardPayment",[881,959,960],{"class":897},"(",[881,962,964],{"class":963},"sacAq","BaseModel",[881,966,967],{"class":897},"):\n",[881,969,971,974,977],{"class":704,"line":970},9,[881,972,973],{"class":897},"    method: Literal[",[881,975,976],{"class":886},"\"card\"",[881,978,979],{"class":897},"]\n",[881,981,983,986,989,992,995,998,1001,1004,1007,1010,1012,1015],{"class":704,"line":982},10,[881,984,985],{"class":897},"    card_number: ",[881,987,988],{"class":963},"str",[881,990,991],{"class":893}," =",[881,993,994],{"class":897}," Field(",[881,996,997],{"class":956},"min_length",[881,999,1000],{"class":893},"=",[881,1002,1003],{"class":963},"12",[881,1005,1006],{"class":897},", ",[881,1008,1009],{"class":956},"max_length",[881,1011,1000],{"class":893},[881,1013,1014],{"class":963},"19",[881,1016,1017],{"class":897},")\n",[881,1019,1021,1024,1026,1028,1030,1032,1034,1037,1039,1041,1043,1046],{"class":704,"line":1020},11,[881,1022,1023],{"class":897},"    cvc: ",[881,1025,988],{"class":963},[881,1027,991],{"class":893},[881,1029,994],{"class":897},[881,1031,997],{"class":956},[881,1033,1000],{"class":893},[881,1035,1036],{"class":963},"3",[881,1038,1006],{"class":897},[881,1040,1009],{"class":956},[881,1042,1000],{"class":893},[881,1044,1045],{"class":963},"4",[881,1047,1017],{"class":897},[881,1049,1051],{"class":704,"line":1050},12,[881,1052,911],{"emptyLinePlaceholder":910},[881,1054,1056],{"class":704,"line":1055},13,[881,1057,911],{"emptyLinePlaceholder":910},[881,1059,1061,1063,1066,1068,1070],{"class":704,"line":1060},14,[881,1062,953],{"class":893},[881,1064,1065],{"class":956}," BankTransferPayment",[881,1067,960],{"class":897},[881,1069,964],{"class":963},[881,1071,967],{"class":897},[881,1073,1075,1077,1080],{"class":704,"line":1074},15,[881,1076,973],{"class":897},[881,1078,1079],{"class":886},"\"bank_transfer\"",[881,1081,979],{"class":897},[881,1083,1085,1088,1090,1092,1094,1096,1098,1100],{"class":704,"line":1084},16,[881,1086,1087],{"class":897},"    iban: ",[881,1089,988],{"class":963},[881,1091,991],{"class":893},[881,1093,994],{"class":897},[881,1095,997],{"class":956},[881,1097,1000],{"class":893},[881,1099,718],{"class":963},[881,1101,1017],{"class":897},[881,1103,1105,1108],{"class":704,"line":1104},17,[881,1106,1107],{"class":897},"    reference: ",[881,1109,1110],{"class":963},"str\n",[881,1112,1114],{"class":704,"line":1113},18,[881,1115,911],{"emptyLinePlaceholder":910},[881,1117,1119],{"class":704,"line":1118},19,[881,1120,911],{"emptyLinePlaceholder":910},[881,1122,1124,1126,1129,1131,1133],{"class":704,"line":1123},20,[881,1125,953],{"class":893},[881,1127,1128],{"class":956}," WalletPayment",[881,1130,960],{"class":897},[881,1132,964],{"class":963},[881,1134,967],{"class":897},[881,1136,1138,1140,1143],{"class":704,"line":1137},21,[881,1139,973],{"class":897},[881,1141,1142],{"class":886},"\"wallet\"",[881,1144,979],{"class":897},[881,1146,1148,1151],{"class":704,"line":1147},22,[881,1149,1150],{"class":897},"    wallet_id: ",[881,1152,1110],{"class":963},[881,1154,1156,1159],{"class":704,"line":1155},23,[881,1157,1158],{"class":897},"    device_token: ",[881,1160,1110],{"class":963},[881,1162,1164],{"class":704,"line":1163},24,[881,1165,911],{"emptyLinePlaceholder":910},[881,1167,1169],{"class":704,"line":1168},25,[881,1170,911],{"emptyLinePlaceholder":910},[881,1172,1174,1177,1179],{"class":704,"line":1173},26,[881,1175,1176],{"class":897},"Tagged ",[881,1178,1000],{"class":893},[881,1180,1181],{"class":897}," Annotated[\n",[881,1183,1185],{"class":704,"line":1184},27,[881,1186,1187],{"class":897},"    Union[CardPayment, BankTransferPayment, WalletPayment],\n",[881,1189,1191,1194,1196,1198,1201],{"class":704,"line":1190},28,[881,1192,1193],{"class":897},"    Field(",[881,1195,616],{"class":956},[881,1197,1000],{"class":893},[881,1199,1200],{"class":886},"\"method\"",[881,1202,1203],{"class":897},"),\n",[881,1205,1207],{"class":704,"line":1206},29,[881,1208,979],{"class":897},[881,1210,1212,1215,1217],{"class":704,"line":1211},30,[881,1213,1214],{"class":897},"Untagged ",[881,1216,1000],{"class":893},[881,1218,1219],{"class":897}," Union[CardPayment, BankTransferPayment, WalletPayment]\n",[881,1221,1223],{"class":704,"line":1222},31,[881,1224,911],{"emptyLinePlaceholder":910},[881,1226,1228],{"class":704,"line":1227},32,[881,1229,911],{"emptyLinePlaceholder":910},[881,1231,1233,1235,1238,1240,1242],{"class":704,"line":1232},33,[881,1234,953],{"class":893},[881,1236,1237],{"class":956}," TaggedEnvelope",[881,1239,960],{"class":897},[881,1241,964],{"class":963},[881,1243,967],{"class":897},[881,1245,1247,1250],{"class":704,"line":1246},34,[881,1248,1249],{"class":897},"    amount_cents: ",[881,1251,1252],{"class":963},"int\n",[881,1254,1256],{"class":704,"line":1255},35,[881,1257,1258],{"class":897},"    payment: Tagged\n",[881,1260,1262],{"class":704,"line":1261},36,[881,1263,911],{"emptyLinePlaceholder":910},[881,1265,1267],{"class":704,"line":1266},37,[881,1268,911],{"emptyLinePlaceholder":910},[881,1270,1272,1274,1277,1279,1281],{"class":704,"line":1271},38,[881,1273,953],{"class":893},[881,1275,1276],{"class":956}," UntaggedEnvelope",[881,1278,960],{"class":897},[881,1280,964],{"class":963},[881,1282,967],{"class":897},[881,1284,1286,1288],{"class":704,"line":1285},39,[881,1287,1249],{"class":897},[881,1289,1252],{"class":963},[881,1291,1293],{"class":704,"line":1292},40,[881,1294,1295],{"class":897},"    payment: Untagged\n",[590,1297,1298],{},"Both envelopes are served from the same app, so the comparison below is genuinely like-for-like: identical models, identical payload, one annotation different.",[1300,1301,1303],"h3",{"id":1302},"the-tagged-422","The tagged 422",[872,1305,1309],{"className":1306,"code":1308,"language":676,"meta":877},[1307],"language-text","$ POST \u002Ftagged  {\"amount_cents\": 1999, \"payment\": {\"method\": \"card\", \"card_number\": \"42\", \"cvc\": \"1\"}}\n422 Unprocessable Entity\n{\n  \"detail\": [\n    {\n      \"type\": \"string_too_short\",\n      \"loc\": [\n        \"body\",\n        \"payment\",\n        \"card\",\n        \"card_number\"\n      ],\n      \"msg\": \"String should have at least 12 characters\",\n      \"input\": \"42\",\n      \"ctx\": {\n        \"min_length\": 12\n      }\n    },\n    {\n      \"type\": \"string_too_short\",\n      \"loc\": [\n        \"body\",\n        \"payment\",\n        \"card\",\n        \"cvc\"\n      ],\n      \"msg\": \"String should have at least 3 characters\",\n      \"input\": \"1\",\n      \"ctx\": {\n        \"min_length\": 3\n      }\n    }\n  ]\n}\n",[603,1310,1308],{"__ignoreMap":877},[590,1312,1313,1314,1317],{},"Two errors, both true, both about the thing the caller actually sent. The location path reads ",[603,1315,1316],{},"body.payment.card.card_number"," — the tag value appears as the path segment, so a client can map the error straight onto its own form field without knowing your class names.",[1300,1319,1321],{"id":1320},"the-untagged-422-same-payload","The untagged 422, same payload",[872,1323,1326],{"className":1324,"code":1325,"language":676,"meta":877},[1307],"$ POST \u002Funtagged  {\"amount_cents\": 1999, \"payment\": {\"method\": \"card\", \"card_number\": \"42\", \"cvc\": \"1\"}}\n422 Unprocessable Entity\n{\n  \"detail\": [\n    {\n      \"type\": \"string_too_short\",\n      \"loc\": [\n        \"body\",\n        \"payment\",\n        \"CardPayment\",\n        \"card_number\"\n      ],\n      \"msg\": \"String should have at least 12 characters\",\n      \"input\": \"42\",\n      \"ctx\": {\n        \"min_length\": 12\n      }\n    },\n    {\n      \"type\": \"string_too_short\",\n      \"loc\": [\n        \"body\",\n        \"payment\",\n        \"CardPayment\",\n        \"cvc\"\n      ],\n      \"msg\": \"String should have at least 3 characters\",\n      \"input\": \"1\",\n      \"ctx\": {\n        \"min_length\": 3\n      }\n    },\n    {\n      \"type\": \"literal_error\",\n      \"loc\": [\n        \"body\",\n        \"payment\",\n        \"BankTransferPayment\",\n        \"method\"\n      ],\n      \"msg\": \"Input should be 'bank_transfer'\",\n      \"input\": \"card\",\n      \"ctx\": {\n        \"expected\": \"'bank_transfer'\"\n      }\n    },\n    {\n      \"type\": \"missing\",\n      \"loc\": [\n        \"body\",\n        \"payment\",\n        \"BankTransferPayment\",\n        \"iban\"\n      ],\n      \"msg\": \"Field required\",\n      \"input\": {\n        \"method\": \"card\",\n        \"card_number\": \"42\",\n        \"cvc\": \"1\"\n      }\n    },\n    {\n      \"type\": \"missing\",\n      \"loc\": [\n        \"body\",\n        \"payment\",\n        \"BankTransferPayment\",\n        \"reference\"\n      ],\n      \"msg\": \"Field required\",\n      \"input\": {\n        \"method\": \"card\",\n        \"card_number\": \"42\",\n        \"cvc\": \"1\"\n      }\n    },\n    {\n      \"type\": \"literal_error\",\n      \"loc\": [\n        \"body\",\n        \"payment\",\n        \"WalletPayment\",\n        \"method\"\n      ],\n      \"msg\": \"Input should be 'wallet'\",\n      \"input\": \"card\",\n      \"ctx\": {\n        \"expected\": \"'wallet'\"\n      }\n    },\n    {\n      \"type\": \"missing\",\n      \"loc\": [\n        \"body\",\n        \"payment\",\n        \"WalletPayment\",\n        \"wallet_id\"\n      ],\n      \"msg\": \"Field required\",\n      \"input\": {\n        \"method\": \"card\",\n        \"card_number\": \"42\",\n        \"cvc\": \"1\"\n      }\n    },\n    {\n      \"type\": \"missing\",\n      \"loc\": [\n        \"body\",\n        \"payment\",\n        \"WalletPayment\",\n        \"device_token\"\n      ],\n      \"msg\": \"Field required\",\n      \"input\": {\n        \"method\": \"card\",\n        \"card_number\": \"42\",\n        \"cvc\": \"1\"\n      }\n    }\n  ]\n}\n",[603,1327,1325],{"__ignoreMap":877},[590,1329,1330,1331,1334],{},"Eight errors for one mistake. Six of them tell a caller who is paying by card that they forgot their IBAN. The class names leak into the response, so ",[603,1332,1333],{},"CardPayment"," — an implementation detail — becomes part of your public error contract.",[1300,1336,1338],{"id":1337},"the-unknown-tag","The unknown tag",[590,1340,1341],{},"The tagged version also gets an error class the untagged one cannot produce at all:",[872,1343,1346],{"className":1344,"code":1345,"language":676,"meta":877},[1307],"$ POST \u002Ftagged  {\"amount_cents\": 1999, \"payment\": {\"method\": \"cheque\", \"amount\": 1}}\n422 Unprocessable Entity\n{\n  \"detail\": [\n    {\n      \"type\": \"union_tag_invalid\",\n      \"loc\": [\n        \"body\",\n        \"payment\"\n      ],\n      \"msg\": \"Input tag 'cheque' found using 'method' does not match any of the expected tags: 'card', 'bank_transfer', 'wallet'\",\n      \"input\": {\n        \"method\": \"cheque\",\n        \"amount\": 1\n      },\n      \"ctx\": {\n        \"discriminator\": \"'method'\",\n        \"tag\": \"cheque\",\n        \"expected_tags\": \"'card', 'bank_transfer', 'wallet'\"\n      }\n    }\n  ]\n}\n",[603,1347,1345],{"__ignoreMap":877},[590,1349,1350],{},"One error that names what was sent, names the field it was read from, and lists every acceptable value. There is nothing left for the caller to guess. The untagged union's answer to the same request is another eight-error pile-up.",[1300,1352,1354],{"id":1353},"the-generated-schema","The generated schema",[872,1356,1359],{"className":1357,"code":1358,"language":676,"meta":877},[1307],"$ GET \u002F_meta\u002Fschema\n200 OK\n{\n  \"tagged_payment_property\": {\n    \"oneOf\": [\n      {\n        \"$ref\": \"#\u002Fcomponents\u002Fschemas\u002FCardPayment\"\n      },\n      {\n        \"$ref\": \"#\u002Fcomponents\u002Fschemas\u002FBankTransferPayment\"\n      },\n      {\n        \"$ref\": \"#\u002Fcomponents\u002Fschemas\u002FWalletPayment\"\n      }\n    ],\n    \"title\": \"Payment\",\n    \"discriminator\": {\n      \"propertyName\": \"method\",\n      \"mapping\": {\n        \"bank_transfer\": \"#\u002Fcomponents\u002Fschemas\u002FBankTransferPayment\",\n        \"card\": \"#\u002Fcomponents\u002Fschemas\u002FCardPayment\",\n        \"wallet\": \"#\u002Fcomponents\u002Fschemas\u002FWalletPayment\"\n      }\n    }\n  },\n  \"untagged_payment_property\": {\n    \"anyOf\": [\n      {\n        \"$ref\": \"#\u002Fcomponents\u002Fschemas\u002FCardPayment\"\n      },\n      {\n        \"$ref\": \"#\u002Fcomponents\u002Fschemas\u002FBankTransferPayment\"\n      },\n      {\n        \"$ref\": \"#\u002Fcomponents\u002Fschemas\u002FWalletPayment\"\n      }\n    ],\n    \"title\": \"Payment\"\n  }\n}\n",[603,1360,1358],{"__ignoreMap":877},[590,1362,1363,1364,1366,1367,1370,1371,1374,1375,1378,1379,1381],{},"The ",[603,1365,856],{}," is the deliverable. A TypeScript generator reading it emits a discriminated union that narrows on ",[603,1368,1369],{},"payment.method",", so a consumer writing ",[603,1372,1373],{},"if (payment.method === \"card\")"," gets ",[603,1376,1377],{},"card_number"," autocompleted. Reading the ",[603,1380,839],{}," version, the best a generator can do is a bare union the caller must interrogate by hand.",[645,1383,1385],{"id":1384},"verification","Verification",[590,1387,1388],{},"Assert the schema shape, because it is what downstream generators consume:",[872,1390,1392],{"className":874,"code":1391,"language":876,"meta":877,"style":877},"def test_schema_carries_a_discriminator_mapping():\n    prop = app.openapi()[\"components\"][\"schemas\"][\"TaggedEnvelope\"][\"properties\"][\"payment\"]\n    assert prop[\"discriminator\"][\"propertyName\"] == \"method\"\n    assert set(prop[\"discriminator\"][\"mapping\"]) == {\"card\", \"bank_transfer\", \"wallet\"}\n",[603,1393,1394,1406,1442,1467],{"__ignoreMap":877},[881,1395,1396,1399,1403],{"class":704,"line":883},[881,1397,1398],{"class":893},"def",[881,1400,1402],{"class":1401},"s3dhs"," test_schema_carries_a_discriminator_mapping",[881,1404,1405],{"class":897},"():\n",[881,1407,1408,1411,1413,1416,1419,1422,1425,1427,1430,1432,1435,1437,1440],{"class":704,"line":890},[881,1409,1410],{"class":897},"    prop ",[881,1412,1000],{"class":893},[881,1414,1415],{"class":897}," app.openapi()[",[881,1417,1418],{"class":886},"\"components\"",[881,1420,1421],{"class":897},"][",[881,1423,1424],{"class":886},"\"schemas\"",[881,1426,1421],{"class":897},[881,1428,1429],{"class":886},"\"TaggedEnvelope\"",[881,1431,1421],{"class":897},[881,1433,1434],{"class":886},"\"properties\"",[881,1436,1421],{"class":897},[881,1438,1439],{"class":886},"\"payment\"",[881,1441,979],{"class":897},[881,1443,1444,1447,1450,1453,1455,1458,1461,1464],{"class":704,"line":907},[881,1445,1446],{"class":893},"    assert",[881,1448,1449],{"class":897}," prop[",[881,1451,1452],{"class":886},"\"discriminator\"",[881,1454,1421],{"class":897},[881,1456,1457],{"class":886},"\"propertyName\"",[881,1459,1460],{"class":897},"] ",[881,1462,1463],{"class":893},"==",[881,1465,1466],{"class":886}," \"method\"\n",[881,1468,1469,1471,1474,1477,1479,1481,1484,1487,1489,1492,1494,1496,1498,1500,1502],{"class":704,"line":914},[881,1470,1446],{"class":893},[881,1472,1473],{"class":963}," set",[881,1475,1476],{"class":897},"(prop[",[881,1478,1452],{"class":886},[881,1480,1421],{"class":897},[881,1482,1483],{"class":886},"\"mapping\"",[881,1485,1486],{"class":897},"]) ",[881,1488,1463],{"class":893},[881,1490,1491],{"class":897}," {",[881,1493,976],{"class":886},[881,1495,1006],{"class":897},[881,1497,1079],{"class":886},[881,1499,1006],{"class":897},[881,1501,1142],{"class":886},[881,1503,1504],{"class":897},"}\n",[590,1506,1507,1508,1510,1511,1514],{},"Then assert the error quality, which is the property that regresses silently if somebody adds a member without a ",[603,1509,633],{}," or drops the ",[603,1512,1513],{},"Field(discriminator=...)"," during a refactor:",[872,1516,1518],{"className":874,"code":1517,"language":876,"meta":877,"style":877},"def test_bad_card_reports_only_card_errors(client):\n    body = {\"amount_cents\": 1, \"payment\": {\"method\": \"card\", \"card_number\": \"42\", \"cvc\": \"1\"}}\n    detail = client.post(\"\u002Ftagged\", json=body).json()[\"detail\"]\n    assert all(error[\"loc\"][2] == \"card\" for error in detail)\n    assert len(detail) == 2\n",[603,1519,1520,1530,1584,1612,1648],{"__ignoreMap":877},[881,1521,1522,1524,1527],{"class":704,"line":883},[881,1523,1398],{"class":893},[881,1525,1526],{"class":1401}," test_bad_card_reports_only_card_errors",[881,1528,1529],{"class":897},"(client):\n",[881,1531,1532,1535,1537,1539,1542,1545,1548,1550,1552,1555,1557,1559,1561,1563,1566,1568,1571,1573,1576,1578,1581],{"class":704,"line":890},[881,1533,1534],{"class":897},"    body ",[881,1536,1000],{"class":893},[881,1538,1491],{"class":897},[881,1540,1541],{"class":886},"\"amount_cents\"",[881,1543,1544],{"class":897},": ",[881,1546,1547],{"class":963},"1",[881,1549,1006],{"class":897},[881,1551,1439],{"class":886},[881,1553,1554],{"class":897},": {",[881,1556,1200],{"class":886},[881,1558,1544],{"class":897},[881,1560,976],{"class":886},[881,1562,1006],{"class":897},[881,1564,1565],{"class":886},"\"card_number\"",[881,1567,1544],{"class":897},[881,1569,1570],{"class":886},"\"42\"",[881,1572,1006],{"class":897},[881,1574,1575],{"class":886},"\"cvc\"",[881,1577,1544],{"class":897},[881,1579,1580],{"class":886},"\"1\"",[881,1582,1583],{"class":897},"}}\n",[881,1585,1586,1589,1591,1594,1597,1599,1602,1604,1607,1610],{"class":704,"line":907},[881,1587,1588],{"class":897},"    detail ",[881,1590,1000],{"class":893},[881,1592,1593],{"class":897}," client.post(",[881,1595,1596],{"class":886},"\"\u002Ftagged\"",[881,1598,1006],{"class":897},[881,1600,1601],{"class":956},"json",[881,1603,1000],{"class":893},[881,1605,1606],{"class":897},"body).json()[",[881,1608,1609],{"class":886},"\"detail\"",[881,1611,979],{"class":897},[881,1613,1614,1616,1619,1622,1625,1627,1629,1631,1633,1636,1639,1642,1645],{"class":704,"line":914},[881,1615,1446],{"class":893},[881,1617,1618],{"class":963}," all",[881,1620,1621],{"class":897},"(error[",[881,1623,1624],{"class":886},"\"loc\"",[881,1626,1421],{"class":897},[881,1628,759],{"class":963},[881,1630,1460],{"class":897},[881,1632,1463],{"class":893},[881,1634,1635],{"class":886}," \"card\"",[881,1637,1638],{"class":893}," for",[881,1640,1641],{"class":897}," error ",[881,1643,1644],{"class":893},"in",[881,1646,1647],{"class":897}," detail)\n",[881,1649,1650,1652,1655,1658,1660],{"class":704,"line":927},[881,1651,1446],{"class":893},[881,1653,1654],{"class":963}," len",[881,1656,1657],{"class":897},"(detail) ",[881,1659,1463],{"class":893},[881,1661,1662],{"class":963}," 2\n",[590,1664,1665],{},"A final guard worth having: assert that the mapping covers every member. A new variant added to the union but missing from the tag literals is a runtime error the moment someone sends it, and a one-line test catches it at build time.",[645,1667,1669],{"id":1668},"trade-offs-and-when-not-to","Trade-offs and When Not To",[590,1671,1672,1675,1676,1678],{},[593,1673,1674],{},"The discriminator field must exist and must be literal."," Every member needs the same field name annotated with ",[603,1677,633],{},", non-optional, with a distinct value. If your payload does not already carry a type tag, adding one is a request-format change your clients have to make — real work, and the main reason to design the tag in from the start.",[590,1680,1681,1684,1685,1688,1689,1692,1693,1697],{},[593,1682,1683],{},"Some payloads genuinely have no tag."," Accepting either ",[603,1686,1687],{},"{\"id\": 1}"," or ",[603,1690,1691],{},"{\"slug\": \"abc\"}"," is structural, not tagged. There a plain union is correct, and the way to improve the error is a custom validator that inspects the keys and raises one clear message. ",[639,1694,1696],{"href":1695},"\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fcross-field-validation-patterns\u002F","Cross-Field Validation Patterns"," covers that shape.",[590,1699,1700,1703],{},[593,1701,1702],{},"Adding a variant is a schema change."," New tags are additive for servers and breaking for strict clients, which will reject an unrecognised tag. Plan the rollout: ship the client's tolerance for unknown tags before you ship the tag.",[590,1705,1706,1709,1710,1006,1712,1715,1716,1719],{},[593,1707,1708],{},"Class names leak in untagged unions."," This is worth restating as a security-adjacent point rather than an ergonomic one. The untagged 422 above published ",[603,1711,1333],{},[603,1713,1714],{},"BankTransferPayment"," and ",[603,1717,1718],{},"WalletPayment"," to any unauthenticated caller who sent a malformed body. Tagged unions publish your tag vocabulary instead, which is already part of the public contract.",[590,1721,1722,1725],{},[593,1723,1724],{},"Nested discriminated unions get hard to read."," A union of unions with different discriminator fields validates correctly but produces error paths several segments deep. Flatten where you can, and give each level a distinctly-named tag.",[590,1727,1728,1729,1733],{},"If you also reshape the 422 envelope itself — grouping errors by field, say — see ",[639,1730,1732],{"href":1731},"\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fcustomising-validation-error-responses\u002F","Customising Validation Error Responses",", and note that a tagged union makes that reshaping far simpler because there is only ever one variant's worth of errors to group.",[645,1735,1737],{"id":1736},"faq","FAQ",[590,1739,1740,1743,1744,613,1746,1748,1749,1751],{},[593,1741,1742],{},"What does Field(discriminator=...) actually change?","\nIt tells Pydantic to read one literal field first and validate against only the matching member, instead of trying every member and collecting all their failures. In the generated schema it emits ",[603,1745,612],{},[603,1747,616],{}," mapping rather than a bare ",[603,1750,839],{},".",[590,1753,1754,1757],{},[593,1755,1756],{},"Why is my 422 full of errors for variants I never sent?","\nBecause an untagged union has no way to know which member you meant, so it validates against all of them and reports every failure. In a real run a bad card payload against a three-member untagged union produced eight errors, six of which were about bank transfers and wallets.",[590,1759,1760,1763,1764,1766,1767,1769],{},[593,1761,1762],{},"What does the discriminator field have to be?","\nA ",[603,1765,633],{},"-typed field present on every member of the union, with a distinct value per member. It cannot be optional, cannot have a plain ",[603,1768,988],{}," annotation, and every member must spell the field name identically.",[590,1771,1772,1775,1776,1778],{},[593,1773,1774],{},"What error do I get for an unknown tag?","\nA single ",[603,1777,626],{}," error naming the tag you sent and listing every tag that would have been accepted, located at the union field itself rather than inside any member. It is the most actionable 422 in Pydantic.",[590,1780,1781,1784,1785,1787,1788,1791],{},[593,1782,1783],{},"Do discriminated unions help client code generation?","\nSubstantially. The ",[603,1786,616],{}," mapping in the OpenAPI document tells a generator which concrete type corresponds to which tag value, so it can emit a proper tagged union with narrowing instead of an untyped ",[603,1789,1790],{},"any"," or a union the caller must inspect by hand.",[645,1793,1795],{"id":1794},"related-reading","Related Reading",[597,1797,1798,1806,1813,1818,1823],{},[600,1799,1800,1803,1804,1751],{},[593,1801,1802],{},"Up to the topic:"," ",[639,1805,642],{"href":641},[600,1807,1808,1809,1751],{},"One named example per variant, so the docs show each tag: ",[639,1810,1812],{"href":1811},"\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fexamples-in-openapi-schema\u002F","Examples in the OpenAPI Schema",[600,1814,1815,1816,1751],{},"Untagged alternatives that need a hand-written check: ",[639,1817,1696],{"href":1695},[600,1819,1820,1821,1751],{},"Reshaping the 422 envelope once the errors are clean: ",[639,1822,1732],{"href":1731},[600,1824,1825,1826,1751],{},"Validating tagged payloads outside a request body: ",[639,1827,1829],{"href":1828},"\u002Fadvanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Ftypeadapter-for-non-model-types\u002F","TypeAdapter for Non-Model Types",[1831,1832,1833],"style",{},"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 .sigWx, html code.shiki .sigWx{--shiki-default:#0E1116}html pre.shiki code .sV4o_, html code.shiki .sV4o_{--shiki-default:#702C00}html pre.shiki code .sacAq, html code.shiki .sacAq{--shiki-default:#023B95}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s3dhs, html code.shiki .s3dhs{--shiki-default:#622CBC}",{"title":877,"searchDepth":890,"depth":890,"links":1835},[1836,1837,1838,1844,1845,1846,1847],{"id":647,"depth":890,"text":648},{"id":801,"depth":890,"text":802},{"id":863,"depth":890,"text":864,"children":1839},[1840,1841,1842,1843],{"id":1302,"depth":907,"text":1303},{"id":1320,"depth":907,"text":1321},{"id":1337,"depth":907,"text":1338},{"id":1353,"depth":907,"text":1354},{"id":1384,"depth":890,"text":1385},{"id":1668,"depth":890,"text":1669},{"id":1736,"depth":890,"text":1737},{"id":1794,"depth":890,"text":1795},"2026-07-20","Use Field(discriminator=...) for tagged unions in FastAPI: a oneOf schema with a discriminator mapping, and a 422 naming one variant, not every member.","md",[1852,1854,1856,1858,1860],{"q":1742,"a":1853},"It tells Pydantic to read one literal field first and validate against only the matching member, instead of trying every member and collecting all their failures. In the generated schema it emits oneOf plus a discriminator mapping rather than a bare anyOf.",{"q":1756,"a":1855},"Because an untagged union has no way to know which member you meant, so it validates against all of them and reports every failure. In a real run a bad card payload against a three-member untagged union produced eight errors, six of which were about bank transfers and wallets.",{"q":1762,"a":1857},"A Literal-typed field present on every member of the union, with a distinct value per member. It cannot be optional, cannot have a plain str annotation, and every member must spell the field name identically.",{"q":1774,"a":1859},"A single union_tag_invalid error naming the tag you sent and listing every tag that would have been accepted, located at the union field itself rather than inside any member. It is the most actionable 422 in Pydantic.",{"q":1783,"a":1861},"Substantially. The discriminator mapping in the OpenAPI document tells a generator which concrete type corresponds to which tag value, so it can emit a proper tagged union with narrowing instead of an untyped any or a union the caller must inspect by hand.",null,{"slug":1864,"breadcrumb":1865},"discriminated-unions-in-openapi",[1866,1869,1872,1873],{"label":1867,"path":1868},"Home","\u002F",{"label":1870,"path":1871},"Advanced Pydantic Validation & Serialization","\u002Fadvanced-pydantic-validation-serialization\u002F",{"label":642,"path":641},{"label":1874,"path":1875},"Discriminated Unions in OpenAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fdiscriminated-unions-in-openapi\u002F",{"title":55,"description":1849},"article","yxjzMU2ePLO5uwtyA-F1CbKkZI4TxK5DwBob3budOIA",[1862,1862],1784588202620]