{"componentChunkName":"component---src-templates-blog-post-js","path":"/blog/2026-09-16-fastapi-pydantic-v2-request-validation/","result":{"data":{"site":{"siteMetadata":{"title":"M.Hassan Ahmed","author":"Hassan11196"}},"markdownRemark":{"id":"82ef316a-8136-5ed6-9669-3204545dd028","excerpt":"Most of the bugs I have chased in a FastAPI backend were not in the interesting code. They were at the boundary. A client sent  as a string instead of a number…","html":"<p>Most of the bugs I have chased in a FastAPI backend were not in the interesting code. They were at the boundary. A client sent <code class=\"language-text\">&quot;limit&quot;: &quot;10&quot;</code> as a string instead of a number, or left out a field the handler assumed was always there. The failure then surfaced three functions deep as a confusing <code class=\"language-text\">TypeError</code> or, worse, a silent wrong answer.</p>\n<p>The fix is almost never a defensive <code class=\"language-text\">if</code> inside the handler. It is declaring what a valid request looks like and letting <a href=\"https://docs.pydantic.dev/latest/\">Pydantic</a> reject everything else before your code runs.</p>\n<p>This post is for people who already write FastAPI endpoints and want to stop treating validation as an afterthought. It covers how validation actually runs with Pydantic v2, the constraints and custom validators worth reaching for, what the <code class=\"language-text\">422</code> response looks like, and the ways it bites you once real traffic shows up. I leaned on this in the <a href=\"/project/archi/\">Archi</a> backend, the RAG (retrieval-augmented generation) copilot I built for CMS operations at CERN, where every query endpoint takes untrusted input straight from a browser.</p>\n<h2>Validation happens before your handler runs</h2>\n<p>The thing to internalize is <em>where</em> validation runs. When you type-annotate a parameter with a Pydantic model, FastAPI reads the raw request body, hands it to Pydantic, and calls your function only if the data survives. If it does not, FastAPI returns a <code class=\"language-text\">422</code>, and your handler is never entered.</p>\n<p><img src=\"/2e39771e296fcf9a7ece69a0c4adc5da/validation-flow.svg\" alt=\"A request flow diagram. Raw JSON bytes go into a Pydantic model box that parses, coerces types, checks Field constraints, and runs custom validators. A green valid branch produces a typed model passed to the handler and a 200 response. A red invalid branch produces a 422 Unprocessable Content response with a detail array containing loc, type, and msg for the offending field. A caption notes the handler only ever sees data that passed every rule.\"></p>\n<p>So a handler like the one below never has to check anything. The <code class=\"language-text\">SearchRequest</code> model declares a required <code class=\"language-text\">query</code> string and an optional <code class=\"language-text\">top_k</code> integer:</p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\"><span class=\"token keyword\">from</span> fastapi <span class=\"token keyword\">import</span> FastAPI\n<span class=\"token keyword\">from</span> pydantic <span class=\"token keyword\">import</span> BaseModel\n\napp <span class=\"token operator\">=</span> FastAPI<span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n\n<span class=\"token keyword\">class</span> <span class=\"token class-name\">SearchRequest</span><span class=\"token punctuation\">(</span>BaseModel<span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span>\n    query<span class=\"token punctuation\">:</span> <span class=\"token builtin\">str</span>\n    top_k<span class=\"token punctuation\">:</span> <span class=\"token builtin\">int</span> <span class=\"token operator\">=</span> <span class=\"token number\">5</span>\n\n<span class=\"token decorator annotation punctuation\">@app<span class=\"token punctuation\">.</span>post</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"/search\"</span><span class=\"token punctuation\">)</span>\n<span class=\"token keyword\">def</span> <span class=\"token function\">search</span><span class=\"token punctuation\">(</span>req<span class=\"token punctuation\">:</span> SearchRequest<span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span>\n    <span class=\"token comment\"># req.query is a str, req.top_k is an int. Guaranteed.</span>\n    <span class=\"token keyword\">return</span> run_retrieval<span class=\"token punctuation\">(</span>req<span class=\"token punctuation\">.</span>query<span class=\"token punctuation\">,</span> k<span class=\"token operator\">=</span>req<span class=\"token punctuation\">.</span>top_k<span class=\"token punctuation\">)</span></code></pre></div>\n<p>Two example requests show how it behaves:</p>\n<ul>\n<li>If a client posts <code class=\"language-text\">{&quot;query&quot;: &quot;argocd sync&quot;, &quot;top_k&quot;: &quot;8&quot;}</code>, Pydantic coerces (converts) <code class=\"language-text\">&quot;8&quot;</code> to <code class=\"language-text\">8</code>, and you get an <code class=\"language-text\">int</code>.</li>\n<li>If it posts <code class=\"language-text\">{&quot;top_k&quot;: 8}</code> with no query, the required <code class=\"language-text\">query</code> field is missing, and the request fails with a <code class=\"language-text\">422</code> before <code class=\"language-text\">search</code> runs.</li>\n</ul>\n<p>That is the whole contract: the annotation <em>is</em> the validation.</p>\n<h2>Put constraints on the field, not in the handler body</h2>\n<p>A type alone is a weak spec. <code class=\"language-text\">top_k: int</code> accepts <code class=\"language-text\">-4</code> and <code class=\"language-text\">1_000_000</code>, and both are nonsense for a retrieval call. Pydantic’s <code class=\"language-text\">Field</code> lets you attach constraints to the type. The modern v2 style combines it with <code class=\"language-text\">Annotated</code>, so the constraint travels with the type rather than sitting in a default value:</p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\"><span class=\"token keyword\">from</span> typing <span class=\"token keyword\">import</span> Annotated\n<span class=\"token keyword\">from</span> pydantic <span class=\"token keyword\">import</span> BaseModel<span class=\"token punctuation\">,</span> Field\n\n<span class=\"token keyword\">class</span> <span class=\"token class-name\">SearchRequest</span><span class=\"token punctuation\">(</span>BaseModel<span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span>\n    query<span class=\"token punctuation\">:</span> Annotated<span class=\"token punctuation\">[</span><span class=\"token builtin\">str</span><span class=\"token punctuation\">,</span> Field<span class=\"token punctuation\">(</span>min_length<span class=\"token operator\">=</span><span class=\"token number\">1</span><span class=\"token punctuation\">,</span> max_length<span class=\"token operator\">=</span><span class=\"token number\">500</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">]</span>\n    top_k<span class=\"token punctuation\">:</span> Annotated<span class=\"token punctuation\">[</span><span class=\"token builtin\">int</span><span class=\"token punctuation\">,</span> Field<span class=\"token punctuation\">(</span>ge<span class=\"token operator\">=</span><span class=\"token number\">1</span><span class=\"token punctuation\">,</span> le<span class=\"token operator\">=</span><span class=\"token number\">50</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">]</span> <span class=\"token operator\">=</span> <span class=\"token number\">5</span>\n    rerank<span class=\"token punctuation\">:</span> <span class=\"token builtin\">bool</span> <span class=\"token operator\">=</span> <span class=\"token boolean\">False</span></code></pre></div>\n<p><code class=\"language-text\">ge</code> and <code class=\"language-text\">le</code> mean greater-or-equal and less-or-equal, and there are <code class=\"language-text\">gt</code> and <code class=\"language-text\">lt</code> too. For strings you also get <code class=\"language-text\">min_length</code>, <code class=\"language-text\">max_length</code>, and <code class=\"language-text\">pattern</code> (a regex the value must match). These are documented in the <a href=\"https://docs.pydantic.dev/latest/concepts/fields/\">Pydantic fields reference</a>. The payoff is that an empty query string or <code class=\"language-text\">top_k: 5000</code> now returns a clear error naming the offending field. Every endpoint uses the same machinery, so you are not rewriting bounds checks by hand.</p>\n<p>There is a real reason to keep these bounds tight. On a RAG endpoint, <code class=\"language-text\">top_k</code> decides how many chunks you pull and stuff into a context window. An unbounded value is not just wrong: it is a way for a caller to blow your token budget or your latency. The validation layer is the cheapest place to enforce that.</p>\n<h2>When a field rule is not enough: validators</h2>\n<p>Some rules cannot be expressed as a single constraint. For those, Pydantic v2 replaced v1’s <code class=\"language-text\">@validator</code> and <code class=\"language-text\">@root_validator</code> with <code class=\"language-text\">@field_validator</code> and <code class=\"language-text\">@model_validator</code>, and the distinction matters. A field validator sees one field. A model validator sees the whole object, which is what you need for rules that span fields.</p>\n<p>In the example below, the field validator rejects timestamps in the future, and the model validator checks that <code class=\"language-text\">start</code> comes before <code class=\"language-text\">end</code>:</p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\"><span class=\"token keyword\">from</span> pydantic <span class=\"token keyword\">import</span> BaseModel<span class=\"token punctuation\">,</span> field_validator<span class=\"token punctuation\">,</span> model_validator\n\n<span class=\"token keyword\">class</span> <span class=\"token class-name\">DateRange</span><span class=\"token punctuation\">(</span>BaseModel<span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span>\n    start<span class=\"token punctuation\">:</span> <span class=\"token builtin\">int</span>  <span class=\"token comment\"># unix seconds</span>\n    end<span class=\"token punctuation\">:</span> <span class=\"token builtin\">int</span>\n\n    <span class=\"token decorator annotation punctuation\">@field_validator</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"start\"</span><span class=\"token punctuation\">,</span> <span class=\"token string\">\"end\"</span><span class=\"token punctuation\">)</span>\n    <span class=\"token decorator annotation punctuation\">@classmethod</span>\n    <span class=\"token keyword\">def</span> <span class=\"token function\">not_in_future</span><span class=\"token punctuation\">(</span>cls<span class=\"token punctuation\">,</span> v<span class=\"token punctuation\">:</span> <span class=\"token builtin\">int</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">-</span><span class=\"token operator\">></span> <span class=\"token builtin\">int</span><span class=\"token punctuation\">:</span>\n        <span class=\"token keyword\">import</span> time\n        <span class=\"token keyword\">if</span> v <span class=\"token operator\">></span> time<span class=\"token punctuation\">.</span>time<span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span>\n            <span class=\"token keyword\">raise</span> ValueError<span class=\"token punctuation\">(</span><span class=\"token string\">\"timestamp is in the future\"</span><span class=\"token punctuation\">)</span>\n        <span class=\"token keyword\">return</span> v\n\n    <span class=\"token decorator annotation punctuation\">@model_validator</span><span class=\"token punctuation\">(</span>mode<span class=\"token operator\">=</span><span class=\"token string\">\"after\"</span><span class=\"token punctuation\">)</span>\n    <span class=\"token keyword\">def</span> <span class=\"token function\">start_before_end</span><span class=\"token punctuation\">(</span>self<span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span>\n        <span class=\"token keyword\">if</span> self<span class=\"token punctuation\">.</span>start <span class=\"token operator\">>=</span> self<span class=\"token punctuation\">.</span>end<span class=\"token punctuation\">:</span>\n            <span class=\"token keyword\">raise</span> ValueError<span class=\"token punctuation\">(</span><span class=\"token string\">\"start must be before end\"</span><span class=\"token punctuation\">)</span>\n        <span class=\"token keyword\">return</span> self</code></pre></div>\n<p>The <code class=\"language-text\">mode=&quot;after&quot;</code> argument means the validator runs <em>after</em> the individual fields have been parsed and coerced, so <code class=\"language-text\">self.start</code> and <code class=\"language-text\">self.end</code> are already integers. There is also <code class=\"language-text\">mode=&quot;before&quot;</code>, which runs on the raw input before parsing. Use it sparingly, for things like normalizing a value that arrives in more than one shape. The <a href=\"https://docs.pydantic.dev/latest/concepts/validators/\">validators documentation</a> covers both.</p>\n<p>Raising a plain <code class=\"language-text\">ValueError</code> inside a validator is enough. FastAPI catches it and folds the message into the <code class=\"language-text\">422</code> response, so you do not throw HTTP exceptions from inside a model.</p>\n<h2>What the client gets back: the 422 response</h2>\n<p>FastAPI’s validation error is a <code class=\"language-text\">422</code>. Under the current HTTP spec, that status is named <a href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/422\">Unprocessable Content</a>. Older tools still call it Unprocessable Entity; it is the same code. The body is a JSON object with a <code class=\"language-text\">detail</code> array, one entry per problem:</p>\n<div class=\"gatsby-highlight\" data-language=\"json\"><pre class=\"language-json\"><code class=\"language-json\"><span class=\"token punctuation\">{</span>\n  <span class=\"token property\">\"detail\"</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span>\n    <span class=\"token punctuation\">{</span>\n      <span class=\"token property\">\"type\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"greater_than_equal\"</span><span class=\"token punctuation\">,</span>\n      <span class=\"token property\">\"loc\"</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span><span class=\"token string\">\"body\"</span><span class=\"token punctuation\">,</span> <span class=\"token string\">\"top_k\"</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n      <span class=\"token property\">\"msg\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"Input should be greater than or equal to 1\"</span><span class=\"token punctuation\">,</span>\n      <span class=\"token property\">\"input\"</span><span class=\"token operator\">:</span> <span class=\"token number\">-3</span>\n    <span class=\"token punctuation\">}</span>\n  <span class=\"token punctuation\">]</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>In each entry, <code class=\"language-text\">loc</code> is the path to the field, <code class=\"language-text\">type</code> is a stable machine-readable code, and <code class=\"language-text\">msg</code> is human text. A frontend can walk <code class=\"language-text\">loc</code> to highlight the exact input that failed, which is far better than showing one generic “invalid request” toast. When I wire this into a React form, I key the errors by the last element of <code class=\"language-text\">loc</code> and render each one next to the matching field.</p>\n<p>If you want a different response envelope, override the handler for <code class=\"language-text\">RequestValidationError</code> once, rather than reshaping errors per endpoint. This one returns the errors under an <code class=\"language-text\">errors</code> key, along with the request path:</p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\"><span class=\"token keyword\">from</span> fastapi<span class=\"token punctuation\">.</span>exceptions <span class=\"token keyword\">import</span> RequestValidationError\n<span class=\"token keyword\">from</span> fastapi<span class=\"token punctuation\">.</span>responses <span class=\"token keyword\">import</span> JSONResponse\n<span class=\"token keyword\">from</span> fastapi <span class=\"token keyword\">import</span> Request\n\n<span class=\"token decorator annotation punctuation\">@app<span class=\"token punctuation\">.</span>exception_handler</span><span class=\"token punctuation\">(</span>RequestValidationError<span class=\"token punctuation\">)</span>\n<span class=\"token keyword\">def</span> <span class=\"token function\">on_validation_error</span><span class=\"token punctuation\">(</span>request<span class=\"token punctuation\">:</span> Request<span class=\"token punctuation\">,</span> exc<span class=\"token punctuation\">:</span> RequestValidationError<span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span>\n    <span class=\"token keyword\">return</span> JSONResponse<span class=\"token punctuation\">(</span>\n        status_code<span class=\"token operator\">=</span><span class=\"token number\">422</span><span class=\"token punctuation\">,</span>\n        content<span class=\"token operator\">=</span><span class=\"token punctuation\">{</span><span class=\"token string\">\"errors\"</span><span class=\"token punctuation\">:</span> exc<span class=\"token punctuation\">.</span>errors<span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span> <span class=\"token string\">\"path\"</span><span class=\"token punctuation\">:</span> <span class=\"token builtin\">str</span><span class=\"token punctuation\">(</span>request<span class=\"token punctuation\">.</span>url<span class=\"token punctuation\">.</span>path<span class=\"token punctuation\">)</span><span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">)</span></code></pre></div>\n<h2>Failure modes I have actually hit</h2>\n<p><strong>Coercion is not always what you want.</strong> Pydantic v2 is stricter than v1 by default, but it still coerces in lax mode, which is what FastAPI uses for request bodies. A string <code class=\"language-text\">&quot;8&quot;</code> becomes <code class=\"language-text\">8</code>. If you need a hard type match, opt into strict mode with <code class=\"language-text\">Field(strict=True)</code> or <code class=\"language-text\">Strict</code> on the annotation. Read the <a href=\"https://docs.pydantic.dev/latest/concepts/conversion_table/\">conversion table</a> before you assume a type will be rejected; plenty of inputs you expect to fail quietly convert instead.</p>\n<p><strong>Extra fields pass silently by default.</strong> If a client sends a field your model does not declare, Pydantic ignores it. That is usually fine, but on a strict API it hides typos: a caller who sends <code class=\"language-text\">&quot;topk&quot;</code> instead of <code class=\"language-text\">&quot;top_k&quot;</code> gets the default <code class=\"language-text\">5</code> and no error. Set <code class=\"language-text\">model_config = ConfigDict(extra=&quot;forbid&quot;)</code> on the model, and the unexpected field becomes a <code class=\"language-text\">422</code>. I turn this on for internal service-to-service endpoints, where a typo should never be swallowed.</p>\n<p><strong>Validators must return a value.</strong> A <code class=\"language-text\">field_validator</code> must return the value, and a <code class=\"language-text\">model_validator(mode=&quot;after&quot;)</code> must return <code class=\"language-text\">self</code>. Forget the return, and the field becomes <code class=\"language-text\">None</code> or the model comes back empty, with no error to tell you why. This is the single most common Pydantic v2 mistake I review out of pull requests.</p>\n<p><strong>Upgrade FastAPI and Pydantic together.</strong> FastAPI added Pydantic v2 support in <a href=\"https://github.com/fastapi/fastapi/releases/tag/0.100.0\">release 0.100.0</a>. If you are upgrading an older service, bump FastAPI and Pydantic in the same change. A new FastAPI on Pydantic v1, or the reverse, produces import errors and decorators that silently do nothing.</p>\n<h2>Tradeoffs and what I would do differently</h2>\n<p>Validation is not free. Pydantic v2 moved its core into a compiled Rust extension called <a href=\"https://github.com/pydantic/pydantic-core\">pydantic-core</a>, which made it dramatically faster than the pure-Python v1. For normal request bodies, the cost is noise next to a database call or an LLM round trip. Where it does show up is with very large or deeply nested payloads validated on a hot path. If you are ingesting a big batch, validate the envelope strictly and defer parsing the heavy inner items until you actually touch them.</p>\n<p>The thing I got wrong early was over-modeling. It is tempting to encode every business rule into validators, until the model becomes a second copy of your domain logic that drifts from the first. My rule now:</p>\n<ul>\n<li><strong>The model enforces <em>shape and bounds</em>:</strong> the things that make the request well-formed.</li>\n<li><strong>The handler owns rules that need I/O:</strong> a database lookup or another service (“does this user exist”, “is this document indexed”). A validator that does I/O is a validator you cannot reason about.</li>\n</ul>\n<p>Keep the boundary thin and honest, and the rest of the code gets simpler because it can trust its inputs.</p>\n<p>If you want to see this pattern in a real service, the FastAPI backends behind <a href=\"/project/archi/\">Archi</a> and my other <a href=\"/\">LLM and RAG projects</a> all start from the same place: a typed request model at every entry point. The messy validation lives in one declarative spot instead of being scattered through the handlers.</p>\n<p><em>Diagram is my own, made for this post. No external image was used.</em></p>","frontmatter":{"title":"FastAPI Request Validation with Pydantic v2","date":"2026-09-16T00:00:00.000Z","description":"FastAPI validates request bodies with Pydantic v2 before your handler runs. Models, Field constraints, custom validators, the 422 response, and failure modes.","thumbnail":{"childImageSharp":{"fluid":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsSAAALEgHS3X78AAAB1UlEQVQoz1WQa2+bMBSG+ZQABttgbCAYsLkXwiXptClpk3btqlb7/z9oh0SqNOnRq1c+57ElG7QbAa8bcd2L6dj//qifX6unF8jidGkvb9ChlOdr9/Lev/5h/UTawbtZho0lwing8SpO9/v52o/P3XB+GJ+q7tcwX6H306UZztCH+cLjB8LKu2UgP7vjBtplyg1KzCuHaSIqImqXaSpKwkscaLpmQXjhBsrxM8BANEVEIrKmTaTIpkgvVDSRmoWa/bBN8jFORzCTfIrk3vXVFssNliZJDRAQS+0VibwUBRmKtE0Sm+dOqCySeCwPhWJhEQkthMJelvNsSSstcpATtvTl11l/nPy5p8PoHQ+kG+g4e/OMu4ENo5wO6XJM54NcDkLWS6z+dsNRaviwnSuVXzcAFGen3bxCPCO6YeOeZR3NWjtUdphbIodi0gTDe56kVBoWji03Mp0VC0WgOUlhotDmKUq06cRbHG/dCNZsmqyQ2MSx6a4YYFq3GSTMfD352SSKx/bxrfvxXh9fx9PndP7aNT/hnOWzG5emE94tkMNv4FacNGTXMrUXegJ4MYflAgRqorLDuxZ+1HLEfd8wHfE/HNiiYGMHm3vabAUF29vIRPx7+R/odUZAaloa9wAAAABJRU5ErkJggg==","aspectRatio":1.899441340782123,"src":"/static/8007290a8ca1a8e6e05f5955fe127fe7/40a76/hero.png","srcSet":"/static/8007290a8ca1a8e6e05f5955fe127fe7/c972b/hero.png 340w,\n/static/8007290a8ca1a8e6e05f5955fe127fe7/27625/hero.png 680w,\n/static/8007290a8ca1a8e6e05f5955fe127fe7/40a76/hero.png 1360w,\n/static/8007290a8ca1a8e6e05f5955fe127fe7/ed396/hero.png 2000w","sizes":"(max-width: 1360px) 100vw, 1360px"}}}}}},"pageContext":{"slug":"/2026-09-16-fastapi-pydantic-v2-request-validation/","previous":"blog/2026-09-12-fastapi-websockets-streaming-llm-chat/","next":"blog/2026-09-15-grounded-citations-rag/"}},"staticQueryHashes":["32046230"]}