<?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>I’m building an opportunity finder that hides most opportunities</title>
      <dc:creator>Sanskar Kharya</dc:creator>
      <pubDate>Mon, 21 Sep 2026 09:24:05 +0000</pubDate>
      <link>https://dev.to/sansk_ya/im-building-an-opportunity-finder-that-hides-most-opportunities-5gcm</link>
      <guid>https://dev.to/sansk_ya/im-building-an-opportunity-finder-that-hides-most-opportunities-5gcm</guid>
      <description>&lt;p&gt;Most discovery platforms index everything and bury you in filters. Fuko inverts that — it shows an opportunity only when it matches your real profile. Here's the design and the AWS serverless pipeline behind it.&lt;/p&gt;

&lt;p&gt;Every "find opportunities" platform I've used works the same way: index everything, then hand you filters and a search bar and wish you luck. You end up scrolling a thousand hackathons and issues that have nothing to do with you.&lt;/p&gt;

&lt;p&gt;I'm building &lt;strong&gt;&lt;a href="https://main.d2punv4aorsj85.amplifyapp.com/" rel="noopener noreferrer"&gt;Fuko&lt;/a&gt;&lt;/strong&gt; to do the opposite. It's still in progress, but the core idea is already working and it's the part worth writing about: &lt;strong&gt;an opportunity is shown only when it matches your profile.&lt;/strong&gt; No endless list. A small ranked set, and every result annotated with &lt;em&gt;why&lt;/em&gt; it matched.&lt;/p&gt;

&lt;p&gt;This is a build-in-public post — I'll show what's live, the design decisions behind the matching engine, and the AWS serverless pipeline that keeps it fed. It's open source (&lt;a href="https://github.com/MaybeSomeone-arc18/fuko" rel="noopener noreferrer"&gt;repo here&lt;/a&gt;), and I'll be honest about what's still on the roadmap at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one principle everything hangs on
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Opportunity type is a preference, not a relevance signal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you tell Fuko you're interested in hackathons, that records &lt;em&gt;intent&lt;/em&gt; — it does not make every hackathon relevant to you. Type can raise a result's ranking, but it can never qualify a result on its own. To show up at all, an opportunity needs at least one concrete profile signal: a matched &lt;strong&gt;skill&lt;/strong&gt;, &lt;strong&gt;interest&lt;/strong&gt;, or &lt;strong&gt;location&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Anything that matches on type alone gets dropped. That single rule is what keeps the feed small and honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two streams, classified by how you engage
&lt;/h2&gt;

&lt;p&gt;Rather than sorting by source, Fuko sorts opportunities by &lt;em&gt;engagement model&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Contribute&lt;/strong&gt; — remote, skill-based work: GitHub issues, bounties, contribution campaigns. Ranked by skill and tech overlap. Location is ignored entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Near you&lt;/strong&gt; — in-person events: hackathons, meetups, college competitions. Ranked by location first, then interest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same profile drives both, but each stream weights signals differently, because "what can I contribute to from my laptop" and "what's happening near me this weekend" are genuinely different questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making matches structural, not keyword soup
&lt;/h2&gt;

&lt;p&gt;The easy version of this is substring matching on titles. It's also wrong — &lt;code&gt;cli&lt;/code&gt; matches &lt;code&gt;client&lt;/code&gt;, &lt;code&gt;AWS&lt;/code&gt; gets mangled, and you get garbage reasons. Fuko avoids that in two ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token-accurate comparison.&lt;/strong&gt; Matching runs on whole tokens with safe normalization, so acronyms like &lt;code&gt;CSS&lt;/code&gt;, &lt;code&gt;AWS&lt;/code&gt;, and &lt;code&gt;iOS&lt;/code&gt; survive intact and &lt;code&gt;cli&lt;/code&gt; never matches &lt;code&gt;client&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metadata enrichment.&lt;/strong&gt; GitHub opportunities are enriched with each repo's &lt;em&gt;real&lt;/em&gt; languages and topics — pulled from the API, not inferred from the description. So a reason like &lt;code&gt;Matched on TypeScript · Python&lt;/code&gt; reflects the actual stack of the repo, not a keyword that happened to appear in the title.&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="c1"&gt;// The relevance gate, in spirit:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;matchedSkills&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;matchedInterests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;matchedLocation&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// type-only? dropped.&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;opportunity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opportunity&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;signals&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a &lt;strong&gt;deterministic reveal&lt;/strong&gt;: results load five at a time with true counts. If three qualify, you see three. If nothing qualifies, you get an explicit empty state instead of a padded list. An empty feed is a feature — it means Fuko isn't lying to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AWS pipeline keeping it fed
&lt;/h2&gt;

&lt;p&gt;The frontend is Next.js (App Router) + TypeScript + Tailwind + Framer Motion. The interesting infrastructure is behind it, and it's fully serverless:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB&lt;/strong&gt; stores the opportunities table (&lt;code&gt;ap-south-1&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda + SAM&lt;/strong&gt; run the automated opportunity sync from sources (GitHub issues/campaigns and &lt;a href="https://brabble.ai" rel="noopener noreferrer"&gt;Brabble.ai&lt;/a&gt; for events).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amplify&lt;/strong&gt; hosts the app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enrichment is a separate, resumable step — it writes only on a successful fetch and skips records that already have data, so re-running it is safe and cheap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsx &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env.local scripts/enrich-github.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One decision I'm happy with: a &lt;code&gt;USE_MOCK_DB=true&lt;/code&gt; flag that swaps DynamoDB for an in-memory store. Local development doesn't touch AWS at all, which keeps the inner loop fast and my bill at zero while iterating on matching logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The little touches
&lt;/h2&gt;

&lt;p&gt;Three themes — Light, Dark, and Funky (the last one adds color and motion). Spring-based interactions, including a "considering" transition that actually renders the ranking pass before results resolve, so you &lt;em&gt;see&lt;/em&gt; it thinking. And all motion respects &lt;code&gt;prefers-reduced-motion&lt;/code&gt;, because a discovery tool shouldn't make anyone motion-sick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it honestly stands
&lt;/h2&gt;

&lt;p&gt;What's working today: onboarding, the two-stream feed, the matching engine and relevance gate, themes, and opportunity detail views. The AWS sync pipeline is deployed.&lt;/p&gt;

&lt;p&gt;Still in progress:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuous live sync&lt;/strong&gt; — the Lambda + EventBridge schedule is built; I'm wiring enrichment into every scheduled run so the feed stays current automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editable profiles&lt;/strong&gt; that re-resolve the feed live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A deeper skill/topic taxonomy&lt;/strong&gt; for more precise matching.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the roadmap: Google sign-in with a full guest mode, encrypted portable profiles, saved opportunities with deadline reminders, and more sources (GitLab, Devpost, Unstop).&lt;/p&gt;

&lt;h2&gt;
  
  
  It's open source — contributions welcome
&lt;/h2&gt;

&lt;p&gt;Fuko is MIT licensed and built in the open. If any of this resonates, I'd genuinely appreciate the help — whether that's a PR, an issue, or just telling me the relevance gate is too strict:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/MaybeSomeone-arc18/fuko" rel="noopener noreferrer"&gt;https://github.com/MaybeSomeone-arc18/fuko&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://main.d2punv4aorsj85.amplifyapp.com/" rel="noopener noreferrer"&gt;https://main.d2punv4aorsj85.amplifyapp.com/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good first contributions right now: expanding the skill/topic taxonomy, adding a new opportunity source adapter (GitLab, Devpost, Unstop), or refining the matching normalization. Star it if you want to follow along — more updates as the live sync lands.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>typescript</category>
      <category>nextjs</category>
      <category>serverless</category>
    </item>
    <item>
      <title>What I learned shipping a tiny FFmpeg desktop app</title>
      <dc:creator>Sanskar Kharya</dc:creator>
      <pubDate>Mon, 14 Sep 2026 08:56:33 +0000</pubDate>
      <link>https://dev.to/sansk_ya/what-i-learned-shipping-a-tiny-ffmpeg-desktop-app-1hng</link>
      <guid>https://dev.to/sansk_ya/what-i-learned-shipping-a-tiny-ffmpeg-desktop-app-1hng</guid>
      <description>&lt;p&gt;I kept hitting the same annoyance. Someone sends me a &lt;code&gt;.mov&lt;/code&gt;, I need an &lt;code&gt;.mp4&lt;/code&gt;, and every free option online wants me to upload the file to a server I do not trust. So I built mov2mp4, a small desktop app that converts locally and never touches the network.&lt;/p&gt;

&lt;p&gt;The conversion itself is the easy part. FFmpeg already does it in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mov &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrapping that in something a non-developer can actually use is where the real work lived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bundling FFmpeg so nobody has to install it
&lt;/h2&gt;

&lt;p&gt;My first version just called &lt;code&gt;ffmpeg&lt;/code&gt; from the system path. That works on my machine and almost nobody else's. Most people do not have FFmpeg installed, and I did not want the first step of my app to be "go install a command line tool."&lt;/p&gt;

&lt;p&gt;So I bundled the FFmpeg binary inside the app and pointed the code at the bundled copy instead of the system one. The app got bigger, but the install went from three steps to zero. For a tool aimed at people who just want their file converted, that trade was worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Batch and threading
&lt;/h2&gt;

&lt;p&gt;Once one file worked, people wanted folders. Running conversions one at a time was slow, so I moved to a worker setup that processes several files at once and reports progress per file. The lesson I did not expect: I had to add a clean way to cancel a running job. If you kill the process mid write, you get a half-written &lt;code&gt;.mp4&lt;/code&gt; that looks real until someone tries to play it. Now a cancel deletes the partial output instead of leaving garbage behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody warns you about: unsigned apps
&lt;/h2&gt;

&lt;p&gt;This one cost me the most time. My builds are not code signed yet, so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On macOS, Gatekeeper blocks the app on first open and hides the real "Open" button behind Privacy and Security settings.&lt;/li&gt;
&lt;li&gt;On Windows, SmartScreen throws up a blue "unknown publisher" screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users read that as "this app is broken" or "this app is malware." Neither is true, but the operating system does not give me a friendly way to explain that. For now I wrote a short first launch note in the README and added a small helper so people can get past the warning once. Proper signing is on my list, but certificates cost money and I wanted the tool out the door.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell past me
&lt;/h2&gt;

&lt;p&gt;The converter was a weekend. The packaging, the cancel logic, and the "why is my OS scaring my users" problem took far longer. If you are shipping a desktop tool built on an existing binary, budget your time for the wrapper, not the core.&lt;/p&gt;

&lt;p&gt;Repo is here if you want to poke at it: &lt;a href="https://github.com/MaybeSomeone-arc18/mov2mp4" rel="noopener noreferrer"&gt;https://github.com/MaybeSomeone-arc18/mov2mp4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the website link if you want to use it: &lt;br&gt;
mov2mp4.vercel.app&lt;/p&gt;

</description>
      <category>python</category>
      <category>ffmpeg</category>
      <category>opensource</category>
      <category>desktop</category>
    </item>
    <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>
