<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sanskar Kharya</title>
    <description>The latest articles on DEV Community by Sanskar Kharya (@sansk_ya).</description>
    <link>https://dev.to/sansk_ya</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4071013%2F67906e92-2327-41bd-9ae3-eaa5148a84db.jpg</url>
      <title>DEV Community: Sanskar Kharya</title>
      <link>https://dev.to/sansk_ya</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sansk_ya"/>
    <language>en</language>
    <item>
      <title>The Guide That Sees for You: Building an On-Device Navigation Companion in the Browser</title>
      <dc:creator>Sanskar Kharya</dc:creator>
      <pubDate>Thu, 27 Aug 2026 07:14:22 +0000</pubDate>
      <link>https://dev.to/sansk_ya/the-guide-that-sees-for-you-building-an-on-device-navigation-companion-in-the-browser-2g0g</link>
      <guid>https://dev.to/sansk_ya/the-guide-that-sees-for-you-building-an-on-device-navigation-companion-in-the-browser-2g0g</guid>
      <description>&lt;p&gt;I built a browser app that turns a live camera feed into 3D sound, so a visually impaired person can &lt;em&gt;hear&lt;/em&gt; where things are. It runs entirely on-device — no cloud, no network. What exists today is a working prototype in a web browser; the larger goal is a native phone app running on dedicated AI silicon.&lt;/p&gt;

&lt;p&gt;This post walks through the idea, what actually works right now, and the honest gap between the two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://saartheye-ai.vercel.app" rel="noopener noreferrer"&gt;https://saartheye-ai.vercel.app&lt;/a&gt; &lt;br&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/MaybeSomeone-arc18/saartheye-ai" rel="noopener noreferrer"&gt;https://github.com/MaybeSomeone-arc18/saartheye-ai&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Why a phone in the cloud is too slow to keep you safe
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A safety warning that arrives a second late is useless.&lt;/strong&gt; Most AI vision apps send a camera frame to a server, run the model there, and send the answer back. That round trip takes roughly 1.2–3.5 seconds. A person walking at a normal pace covers nearly five feet in that window — long enough to have already hit the obstacle the app is warning about.&lt;/p&gt;

&lt;p&gt;There are three problems with the cloud approach for real-time navigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency.&lt;/strong&gt; The round trip is slower than the thing it's meant to prevent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead zones.&lt;/strong&gt; Elevators, basements, subways, parking garages — the places you most need help are the places with no signal. A cloud app stops working the moment the connection drops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy.&lt;/strong&gt; A navigation aid you wear all day streams a 24/7 video of your entire life — your home, your workplace, the strangers around you — to someone else's servers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doing everything on the device sidesteps all three at once: no round trip, no signal required, and camera frames never leave the phone. That premise is the whole reason this project exists.&lt;/p&gt;


&lt;h2&gt;
  
  
  What I actually built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Everything happens inside the web page — there's no backend server at all.&lt;/strong&gt; The prototype opens your camera, spots objects in the video, and plays them back to you as spatial audio in real time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vision:&lt;/strong&gt; a YOLO-based object detector running through TensorFlow.js, so inference happens on your machine, not a server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio:&lt;/strong&gt; the browser's Web Audio API, which can place a sound at a point in 3D space around your head.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interface:&lt;/strong&gt; a React layer that draws the detection boxes and mode controls on screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why the browser first?&lt;/strong&gt; It was the fastest way to prove the core idea is real. It let me test the full loop — camera → detection → sound — with zero install, on any laptop or phone, in a few days of building. It's the proof of concept, not the finished product.&lt;/p&gt;


&lt;h2&gt;
  
  
  From pixels to sound
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Finding things: the detector
&lt;/h3&gt;

&lt;p&gt;Each video frame goes through an object detector that returns a list of what it sees and where. For every object, the model gives back a label, a confidence score, and a box (its position and size on screen). That box is the single most important piece of data — everything downstream is built from it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DetectedObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// e.g. 'person', 'car', 'chair'&lt;/span&gt;
  &lt;span class="nl"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// confidence, 0.0 to 1.0&lt;/span&gt;
  &lt;span class="nl"&gt;bbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;// x (top-left)&lt;/span&gt;
    &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;// y (top-left)&lt;/span&gt;
    &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;// width&lt;/span&gt;
    &lt;span class="kr"&gt;number&lt;/span&gt;                &lt;span class="c1"&gt;// height&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nl"&gt;spatialVelocity&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// how fast it's approaching (see below)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Turning a box into a sound you can locate
&lt;/h3&gt;

&lt;p&gt;The trick is echolocation: map where an object sits in the frame to where its sound sits around your head. Two simple mappings do most of the work, and because they're just arithmetic on the box, they're effectively instant.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you hear&lt;/th&gt;
&lt;th&gt;Comes from&lt;/th&gt;
&lt;th&gt;So that…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Left / right (stereo pan)&lt;/td&gt;
&lt;td&gt;The box's horizontal position&lt;/td&gt;
&lt;td&gt;An object on your left sounds on your left&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pitch (high / low)&lt;/td&gt;
&lt;td&gt;The box's size on screen&lt;/td&gt;
&lt;td&gt;A bigger, closer object sounds lower and nearer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Urgency of the pulse&lt;/td&gt;
&lt;td&gt;How fast the object is approaching&lt;/td&gt;
&lt;td&gt;A fast approach sounds more insistent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Under the hood this uses the Web Audio API's &lt;code&gt;PannerNode&lt;/code&gt;, which is designed to position sound in 3D. The result: you don't get a robotic voice saying "chair, two o'clock" — you just hear the chair, roughly where it is.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Telling a threat from a table: velocity vectoring
&lt;/h3&gt;

&lt;p&gt;Distance alone is a bad alarm. A chair one meter away isn't dangerous; a cyclist one meter away &lt;em&gt;and closing&lt;/em&gt; is. The distinction isn't how close something is, but whether it's getting closer — and how fast.&lt;/p&gt;

&lt;p&gt;The prototype keeps a short memory of each object's box across recent frames and watches whether that box is growing. A box that swells quickly frame-over-frame means something is rushing toward you, so it escalates to a sharp, panned warning tone. A box that holds steady is treated as furniture or a standing person and fades into a soft, occasional ping.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Reading the room: contextual modes
&lt;/h3&gt;

&lt;p&gt;An aid that screams at everything gets switched off within a minute. Avoiding that "false-positive fatigue" is a feature, not an afterthought.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Outdoor / navigation:&lt;/strong&gt; high sensitivity to anything moving or in your path — bikes, poles, stairs, people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Social / conversation:&lt;/strong&gt; a person standing still in front of you is a friend, not a hazard, so their alarm is suppressed to a gentle ambient ping. Only a sudden approach breaks through.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The honest gap: prototype vs. blueprint
&lt;/h2&gt;

&lt;p&gt;The architecture I designed is more ambitious than the thing I've shipped, and it's worth being clear about which is which. The browser prototype proves the concept end to end. The native version is the target, not a claim about today.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Prototype (today)&lt;/th&gt;
&lt;th&gt;Blueprint (the goal)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runs on&lt;/td&gt;
&lt;td&gt;Web browser tab&lt;/td&gt;
&lt;td&gt;Native Android app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detection&lt;/td&gt;
&lt;td&gt;YOLO via TensorFlow.js&lt;/td&gt;
&lt;td&gt;Quantized YOLOv12 on the phone's neural chip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reasoning&lt;/td&gt;
&lt;td&gt;Bounding-box math in JavaScript&lt;/td&gt;
&lt;td&gt;On-device small language model (Phi-3.5)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feedback&lt;/td&gt;
&lt;td&gt;3D spatial audio&lt;/td&gt;
&lt;td&gt;Spatial audio + haptic vibration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;Fast, browser-limited (unmeasured)&lt;/td&gt;
&lt;td&gt;Sub-15ms target on dedicated silicon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline&lt;/td&gt;
&lt;td&gt;Yes, once loaded&lt;/td&gt;
&lt;td&gt;Yes, full airplane mode by design&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The prototype already answers the risky question: &lt;em&gt;can bounding boxes become useful, locatable sound in real time?&lt;/em&gt; Yes. The remaining work — native memory handling, offloading the model to a dedicated neural processor, adding haptics — is a systems-engineering climb, not an unknown. The concept is de-risked; the performance is the next mountain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it goes next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Port the pipeline to native&lt;/strong&gt; so camera frames flow straight into the model without the browser's overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offload detection to the phone's neural chip&lt;/strong&gt; to chase the sub-15ms target and free the main processor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add haptics&lt;/strong&gt; so a warning you can feel backs up the one you hear, for loud environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with actual users&lt;/strong&gt;, because the real measure isn't latency — it's whether someone trusts it enough to leave the cane at home.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can build the hard, scary part of an idea in a browser in a weekend and learn whether it's worth pursuing. This started as a question about whether sound could stand in for sight, closely enough to be safe. The prototype says it's possible. The rest is engineering.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The name comes from the Sanskrit&lt;/em&gt; Saarthi &lt;em&gt;— a charioteer, the one who guides — joined with&lt;/em&gt; Eye.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>machinelearning</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why I Don't Put Tenant-Specific Validation in My FastAPI Endpoints</title>
      <dc:creator>Sanskar Kharya</dc:creator>
      <pubDate>Mon, 10 Aug 2026 10:09:21 +0000</pubDate>
      <link>https://dev.to/sansk_ya/why-i-dont-put-tenant-specific-validation-in-my-fastapi-endpoints-5e4l</link>
      <guid>https://dev.to/sansk_ya/why-i-dont-put-tenant-specific-validation-in-my-fastapi-endpoints-5e4l</guid>
      <description>&lt;p&gt;Multi-tenant applications have a way of turning simple endpoints into surprisingly complicated pieces of code.&lt;/p&gt;

&lt;p&gt;At first, tenant-specific validation seems harmless.&lt;/p&gt;

&lt;p&gt;You have two tenants with different limits, so you write something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;max_quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;globex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;max_quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It works.&lt;/p&gt;

&lt;p&gt;The problem isn't the first &lt;code&gt;if&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The problem is what happens after there are ten more rules.&lt;/p&gt;
&lt;h2&gt;
  
  
  When the endpoint starts knowing too much
&lt;/h2&gt;

&lt;p&gt;I prefer an endpoint to answer one main question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What should the application do with this request?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I don't want the route handler to also be responsible for figuring out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which tenant made the request&lt;/li&gt;
&lt;li&gt;whether that tenant is active&lt;/li&gt;
&lt;li&gt;what limits the tenant has&lt;/li&gt;
&lt;li&gt;which validation rules apply&lt;/li&gt;
&lt;li&gt;how those validation failures should be returned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those things are really the endpoint's job.&lt;/p&gt;

&lt;p&gt;They are context and validation concerns.&lt;/p&gt;

&lt;p&gt;Once those responsibilities start accumulating inside a route, even a small endpoint can become difficult to reason about.&lt;/p&gt;
&lt;h2&gt;
  
  
  FastAPI dependencies are a natural boundary
&lt;/h2&gt;

&lt;p&gt;FastAPI's dependency injection provides a useful place to resolve tenant information.&lt;/p&gt;

&lt;p&gt;For example, a request can contain:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-Tenant-ID: globex
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A dependency can turn that header into a strongly typed &lt;code&gt;TenantConfig&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now the rest of the request-processing pipeline doesn't need to repeatedly parse the header or look up the tenant.&lt;/p&gt;

&lt;p&gt;But that still leaves an interesting problem.&lt;/p&gt;

&lt;p&gt;The request body doesn't contain the information we need for every validation rule.&lt;/p&gt;
&lt;h2&gt;
  
  
  Some validation needs context
&lt;/h2&gt;

&lt;p&gt;Consider a simple Pydantic model:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;min_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The model can determine that &lt;code&gt;quantity=0&lt;/code&gt; is invalid without knowing anything about the tenant.&lt;/p&gt;

&lt;p&gt;But suppose:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;acme   → maximum quantity = 100
globex → maximum quantity = 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now the validity of the same request depends on information outside the request body.&lt;/p&gt;

&lt;p&gt;That's where Pydantic v2's validation context becomes useful.&lt;/p&gt;

&lt;p&gt;Instead of hard-coding tenant names into the model, the application can pass the resolved tenant into validation:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The validator can access that information through &lt;code&gt;ValidationInfo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The important part for me isn't the specific API. It's the separation of responsibilities:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FastAPI dependency
        ↓
Resolve tenant
        ↓
TenantConfig
        ↓
Pydantic validation
        ↓
Validated request
        ↓
Endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The endpoint doesn't need to know how any of that happened.&lt;/p&gt;
&lt;h2&gt;
  
  
  But there's a trade-off
&lt;/h2&gt;

&lt;p&gt;I don't think this pattern is automatically the right answer for every FastAPI application.&lt;/p&gt;

&lt;p&gt;To make tenant information available before Pydantic validation, the example intentionally receives the request body as raw data and calls &lt;code&gt;model_validate()&lt;/code&gt; itself.&lt;/p&gt;

&lt;p&gt;That gives us the context we need, but we give up some of FastAPI's automatic request-body handling.&lt;/p&gt;

&lt;p&gt;One noticeable consequence is OpenAPI documentation.&lt;/p&gt;

&lt;p&gt;With a normal Pydantic request model, FastAPI can automatically generate a detailed request schema. With the raw-body approach, the generated request schema is less descriptive.&lt;/p&gt;

&lt;p&gt;That's a real trade-off.&lt;/p&gt;

&lt;p&gt;If automatically generated API documentation were the most important requirement, I'd investigate a different integration.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I actually care about
&lt;/h2&gt;

&lt;p&gt;The goal isn't to eliminate every &lt;code&gt;if&lt;/code&gt; statement from an application.&lt;/p&gt;

&lt;p&gt;It's to keep responsibilities somewhere they make sense.&lt;/p&gt;

&lt;p&gt;I'd rather have an endpoint that looks like:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TenantOrderContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_validated_order&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OrderResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;than one that starts doing all of this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# resolve tenant
&lt;/span&gt;    &lt;span class="c1"&gt;# check tenant status
&lt;/span&gt;    &lt;span class="c1"&gt;# determine limits
&lt;/span&gt;    &lt;span class="c1"&gt;# validate request
&lt;/span&gt;    &lt;span class="c1"&gt;# handle validation failures
&lt;/span&gt;    &lt;span class="c1"&gt;# process order
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The second approach isn't inherently broken.&lt;/p&gt;

&lt;p&gt;It's just carrying more responsibility than the endpoint needs to.&lt;/p&gt;
&lt;h2&gt;
  
  
  The broader lesson
&lt;/h2&gt;

&lt;p&gt;The interesting part of this exercise wasn't really multi-tenancy.&lt;/p&gt;

&lt;p&gt;It was thinking about &lt;strong&gt;where validation gets its information from&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Some rules depend only on the object being validated.&lt;/p&gt;

&lt;p&gt;Others depend on the context surrounding that object.&lt;/p&gt;

&lt;p&gt;When validation depends on external context, putting everything inside the route handler is an easy solution. But it isn't necessarily the cleanest boundary.&lt;/p&gt;

&lt;p&gt;FastAPI dependencies and Pydantic v2 validation context give you another option.&lt;/p&gt;

&lt;p&gt;I built a small working example of this approach with FastAPI, Pydantic v2, and pytest, including tests for tenant-specific limits, invalid requests, inactive tenants, unknown tenants, and malformed payloads.&lt;/p&gt;

&lt;p&gt;The complete implementation is available on GitHub.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/MaybeSomeone-arc18" rel="noopener noreferrer"&gt;
        MaybeSomeone-arc18
      &lt;/a&gt; / &lt;a href="https://github.com/MaybeSomeone-arc18/fastapi-multitenant-validation" rel="noopener noreferrer"&gt;
        fastapi-multitenant-validation
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Type-Safe Multi-Tenant Request Validation with FastAPI &amp;amp; Pydantic v2&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A minimal, production-inspired educational Python example demonstrating type-safe multi-tenant request validation using FastAPI dependencies and Pydantic v2 validation context (&lt;code&gt;info.context&lt;/code&gt;).&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Purpose&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Multi-tenant applications often need to validate incoming API payloads against tenant-specific rules (such as max order quantities, allowed product categories, or rate limits).&lt;/p&gt;

&lt;p&gt;A common anti-pattern is placing &lt;code&gt;if tenant == "acme": ...&lt;/code&gt; conditional branching inside route handlers. This pollutes endpoint logic and duplicates validation code.&lt;/p&gt;

&lt;p&gt;This project demonstrates how to move tenant resolution and tenant-specific schema validation into FastAPI dependencies before the endpoint receives the request. The endpoint stays clean and receives a strongly-typed &lt;code&gt;TenantOrderContext&lt;/code&gt;.&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Request Flow &amp;amp; Architecture&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;Client
  │ (POST /orders, Header: X-Tenant-ID, JSON Body)
  ▼
1. get_tenant Dependency
  │ Reads X-Tenant-ID header &amp;amp; resolves TenantConfig
  │ Rejects missing header (422), unknown tenant (404), or inactive tenant (403)
  ▼
2. get_validated_order Dependency
  │ Receives&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/MaybeSomeone-arc18/fastapi-multitenant-validation" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



</description>
      <category>python</category>
      <category>fastapi</category>
      <category>architecture</category>
      <category>pydantic</category>
    </item>
    <item>
      <title>Type-Safe Multi-Tenant Request Validation in FastAPI with Pydantic v2</title>
      <dc:creator>Sanskar Kharya</dc:creator>
      <pubDate>Mon, 10 Aug 2026 09:42:31 +0000</pubDate>
      <link>https://dev.to/sansk_ya/type-safe-multi-tenant-request-validation-in-fastapi-with-pydantic-v2-1f8a</link>
      <guid>https://dev.to/sansk_ya/type-safe-multi-tenant-request-validation-in-fastapi-with-pydantic-v2-1f8a</guid>
      <description>&lt;p&gt;Multi-tenant APIs often need to apply different validation rules depending on which tenant is making a request.&lt;/p&gt;

&lt;p&gt;For example, imagine an order API where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;acme&lt;/code&gt; can create orders with up to 100 items.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;globex&lt;/code&gt; can create orders with up to 10 items.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A straightforward implementation might put tenant-specific checks directly inside the endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;globex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That works initially, but as the number of tenants and rules grows, the endpoint starts becoming responsible for both handling the request and understanding tenant-specific business rules.&lt;/p&gt;

&lt;p&gt;This tutorial demonstrates a different approach using &lt;strong&gt;FastAPI dependencies and Pydantic v2 validation context&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The goal is to resolve the tenant before validating the request and pass that tenant configuration into Pydantic, while keeping the route handler free of tenant-specific validation logic.&lt;/p&gt;
&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;The request will look like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /orders
X-Tenant-ID: globex
Content-Type: application/json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"product_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prod_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The application will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Resolve the tenant from &lt;code&gt;X-Tenant-ID&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Reject unknown or inactive tenants.&lt;/li&gt;
&lt;li&gt;Validate the request using Pydantic.&lt;/li&gt;
&lt;li&gt;Apply the tenant's validation rules using Pydantic's validation context.&lt;/li&gt;
&lt;li&gt;Pass a fully validated object to the endpoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The complete implementation is available in the &lt;code&gt;fastapi-multitenant-validation&lt;/code&gt; GitHub repository.&lt;/p&gt;
&lt;h2&gt;
  
  
  Project structure
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fastapi-multitenant-validation/
├── app/
│   ├── config.py
│   ├── dependencies.py
│   ├── main.py
│   └── models.py
├── tests/
│   ├── conftest.py
│   └── test_orders.py
├── pyproject.toml
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The example intentionally keeps tenant configuration in memory. There is no database or external service because the goal is to demonstrate the validation architecture rather than build a complete multi-tenant platform.&lt;/p&gt;
&lt;h2&gt;
  
  
  Defining tenant configuration
&lt;/h2&gt;

&lt;p&gt;First, we need a representation of a tenant.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unique tenant identifier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Human-readable tenant name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;max_order_quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;...,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Maximum quantity allowed in a single order&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;...,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Whether the tenant is currently active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For the example, we use a deterministic in-memory registry:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;TENANTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TenantConfig&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TenantConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Acme Corporation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_order_quantity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;globex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TenantConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;globex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Globex Inc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_order_quantity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inactive_co&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TenantConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inactive_co&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Inactive Co&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_order_quantity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The important part is that the tenant configuration contains information that can affect validation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Defining the request model
&lt;/h2&gt;

&lt;p&gt;The request-level constraints belong in our Pydantic model:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ValidationInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_validator&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;min_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@model_validator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;after&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_tenant_limits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ValidationInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OrderCreate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TenantConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_order_quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Quantity &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; exceeds maximum allowed &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_order_quantity&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;) for tenant &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;These rules represent two different kinds of validation.&lt;/p&gt;

&lt;p&gt;General request validation can be performed from the body alone:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product_id → must not be empty
quantity   → must be greater than zero
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Tenant-specific validation needs information outside the body:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;quantity → must not exceed tenant.max_order_quantity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Pydantic v2's validation context gives us a way to provide that external information during validation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Resolving the tenant with a FastAPI dependency
&lt;/h2&gt;

&lt;p&gt;Next, we resolve the tenant from the request header.&lt;/p&gt;

&lt;p&gt;The repository uses FastAPI's required-header behavior directly:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_tenant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;x_tenant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Tenant-ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TenantConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TENANTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_tenant_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tenant &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x_tenant_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tenant &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x_tenant_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; is inactive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Because the header is required, FastAPI itself handles a missing &lt;code&gt;X-Tenant-ID&lt;/code&gt; header and returns a &lt;code&gt;422 Unprocessable Entity&lt;/code&gt; response.&lt;/p&gt;

&lt;p&gt;For an unknown tenant, the dependency returns &lt;code&gt;404&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For an inactive tenant, it returns &lt;code&gt;403&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Most importantly, downstream dependencies now receive a strongly typed &lt;code&gt;TenantConfig&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Connecting tenant resolution to Pydantic validation
&lt;/h2&gt;

&lt;p&gt;This is where the two pieces come together.&lt;/p&gt;

&lt;p&gt;We intentionally receive the request body as a raw dictionary:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That allows the tenant dependency to run before we explicitly invoke Pydantic validation.&lt;/p&gt;

&lt;p&gt;The important part of the dependency is:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RequestValidationError&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_validated_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
    &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TenantConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_tenant&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TenantOrderContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RequestValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;include_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TenantOrderContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The key line is:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The tenant configuration is now available inside Pydantic's &lt;code&gt;ValidationInfo.context&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why catch &lt;code&gt;ValidationError&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Normally, FastAPI automatically handles validation errors generated while processing request parameters.&lt;/p&gt;

&lt;p&gt;Here, however, we are deliberately calling Pydantic ourselves inside a dependency.&lt;/p&gt;

&lt;p&gt;That means we need to catch Pydantic's &lt;code&gt;ValidationError&lt;/code&gt; and convert it into FastAPI's &lt;code&gt;RequestValidationError&lt;/code&gt;. Otherwise, a validation failure could escape the normal request-validation handling and become a &lt;code&gt;500 Internal Server Error&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By converting it to &lt;code&gt;RequestValidationError&lt;/code&gt;, the API consistently returns &lt;code&gt;422 Unprocessable Entity&lt;/code&gt; for both ordinary Pydantic validation failures and tenant-specific validation failures.&lt;/p&gt;
&lt;h2&gt;
  
  
  Keeping the endpoint clean
&lt;/h2&gt;

&lt;p&gt;The endpoint receives a strongly typed &lt;code&gt;TenantOrderContext&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;OrderResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP_201_CREATED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TenantOrderContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_validated_order&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OrderResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OrderResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ord_123456&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;There is no:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;and no:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;globex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The endpoint receives a context whose tenant is already known to be active and whose order has already passed both baseline and tenant-specific validation.&lt;/p&gt;

&lt;p&gt;That separation is the main architectural point of the example.&lt;/p&gt;
&lt;h2&gt;
  
  
  Testing the behavior
&lt;/h2&gt;

&lt;p&gt;The repository includes eight tests covering successful requests and failure cases.&lt;/p&gt;

&lt;p&gt;For example, Globex allows a maximum quantity of 10:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Tenant-ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;globex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod_123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But a quantity of 15 should fail:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Tenant-ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;globex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod_123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The test suite also covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invalid quantities&lt;/li&gt;
&lt;li&gt;missing tenant headers&lt;/li&gt;
&lt;li&gt;unknown tenants&lt;/li&gt;
&lt;li&gt;inactive tenants&lt;/li&gt;
&lt;li&gt;non-object JSON payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete suite passes successfully.&lt;/p&gt;
&lt;h2&gt;
  
  
  An important trade-off
&lt;/h2&gt;

&lt;p&gt;There is a reason we don't simply declare:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderCreate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;as the request body and let FastAPI handle everything automatically.&lt;/p&gt;

&lt;p&gt;For this particular example, we need tenant information before performing tenant-aware Pydantic validation.&lt;/p&gt;

&lt;p&gt;Receiving the raw body and explicitly calling:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;gives us that control.&lt;/p&gt;

&lt;p&gt;The trade-off is that FastAPI's automatically generated OpenAPI documentation does not automatically expose the complete &lt;code&gt;OrderCreate&lt;/code&gt; request schema in the same way that a normal Pydantic request-body parameter would.&lt;/p&gt;

&lt;p&gt;This is intentional in the example. In a production system, you would need to decide whether this validation architecture or richer automatically generated API documentation is more important, or investigate an integration that provides both.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The main idea isn't that every FastAPI application should manually validate request bodies.&lt;/p&gt;

&lt;p&gt;The useful pattern is separating responsibilities:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request
   ↓
Tenant dependency
   ↓
TenantConfig
   ↓
Pydantic validation context
   ↓
Validated TenantOrderContext
   ↓
Clean endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;FastAPI dependencies handle request context, while Pydantic handles validation.&lt;/p&gt;

&lt;p&gt;This becomes particularly useful when validation rules depend on information that isn't contained in the request body itself.&lt;/p&gt;

&lt;p&gt;The complete, tested implementation is available in the &lt;code&gt;fastapi-multitenant-validation&lt;/code&gt; GitHub repository.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/MaybeSomeone-arc18" rel="noopener noreferrer"&gt;
        MaybeSomeone-arc18
      &lt;/a&gt; / &lt;a href="https://github.com/MaybeSomeone-arc18/fastapi-multitenant-validation" rel="noopener noreferrer"&gt;
        fastapi-multitenant-validation
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Type-Safe Multi-Tenant Request Validation with FastAPI &amp;amp; Pydantic v2&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A minimal, production-inspired educational Python example demonstrating type-safe multi-tenant request validation using FastAPI dependencies and Pydantic v2 validation context (&lt;code&gt;info.context&lt;/code&gt;).&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Purpose&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Multi-tenant applications often need to validate incoming API payloads against tenant-specific rules (such as max order quantities, allowed product categories, or rate limits).&lt;/p&gt;

&lt;p&gt;A common anti-pattern is placing &lt;code&gt;if tenant == "acme": ...&lt;/code&gt; conditional branching inside route handlers. This pollutes endpoint logic and duplicates validation code.&lt;/p&gt;

&lt;p&gt;This project demonstrates how to move tenant resolution and tenant-specific schema validation into FastAPI dependencies before the endpoint receives the request. The endpoint stays clean and receives a strongly-typed &lt;code&gt;TenantOrderContext&lt;/code&gt;.&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Request Flow &amp;amp; Architecture&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;Client
  │ (POST /orders, Header: X-Tenant-ID, JSON Body)
  ▼
1. get_tenant Dependency
  │ Reads X-Tenant-ID header &amp;amp; resolves TenantConfig
  │ Rejects missing header (422), unknown tenant (404), or inactive tenant (403)
  ▼
2. get_validated_order Dependency
  │ Receives&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/MaybeSomeone-arc18/fastapi-multitenant-validation" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



</description>
      <category>python</category>
      <category>pydantic</category>
      <category>fastapi</category>
      <category>api</category>
    </item>
  </channel>
</rss>
