A flexible way to handle changing JSON inputs without messy "if" or "switch" statements
When building an API, one of the first things you need to figure out is how to route incoming requests to the right piece of code. This is usually done by checking which fields are present in the request and deciding which method to run. For simple APIs, this is often handled with a few if or switch statements. But once your inputs start to vary and grow more complex, this approach becomes hard to manage.
In my case, I needed to build an API endpoint that could handle multiple types of patient-related data. Sometimes a request had a PatientIdNum, other times it had just a date of birth and zip code, or maybe an invoice number. I wanted a way to let the API decide which method to call based on what was sent, without having to write a long list of conditions in code. So I went with a rule-based approach using a feature in C# called reflection.
What is Dispatching?
"Dispatching" simply means sending something to the right place. In APIs, this usually means figuring out which method should handle a request. A "dispatcher" is just a piece of code that makes this decision.
In my case, I wrote a dispatcher that checks the incoming JSON request, matches it to a rule in a JSON file, and then calls the matching method by name. This keeps the routing logic separate from the rest of the business logic and makes it easier to change.
The Problem with Hardcoded Logic
If you've ever written code like this, you know how quickly it can grow:
if (request.PatientIdNum != null)
return HandleByPatientIdNum(request);
else if (request.PatInfo?.Dob != null && request.PatInfo?.Zip != null)
return HandleByDemographics(request);
else if (request.OmInfo?.FirstOrDefault()?.InvoiceNo != null)
return HandleByInvoice(request);
else
return BadRequest("Insufficient identifiers");
This kind of logic is fine at first, but over time it:
• Gets harder to read and maintain
• Tangles routing and business logic together
• Requires code changes every time the JSON input and rules change
A Simpler Way: Rule-Based Routing
With rule-based routing, you define your routing rules outside of your code, usually in a file. For me, that file is "rules.json". Each rule lists the fields required to trigger a handler method. Here's an example:
[
{
"name": "HandleByPatientIdNum",
"match": ["PatientIdNum"]
},
{
"name": "HandleByDemographics",
"match": ["patinfo.dob", "patinfo.zip"]
},
{
"name": "HandleByInvoice",
"match": ["ominvoice.invoice_no"]
}
]
Basically, each rule says: If these fields are present and not null, then run the method with this "name".
How Reflection Helps
Reflection is a .NET feature that lets your code look at itself while it's running. That means you can find a method by name and call it even if you didn't hardcode it directly. So, if the rule says to call "HandleByPatientIdNum", and that method exists in your controller, you can call it just by using its name (Reflection).
This makes the system very flexible. To add new functionality, you just write a new method and add a new rule to the JSON file (rules.json).
Reflection Scope Note:
For safety and clarity, I restrict the reflection lookup to non-public instance methods only using BindingFlags.NonPublic | BindingFlags.Instance. This avoids exposing public framework or utility methods unintentionally and keeps the dynamic invocation limited to the intended handler methods.
How It Works
The dispatcher loads the rules from the JSON file.
Rule Validation:
Before evaluating incoming requests, I run a simple preflight validator to check for common mistakes in the "rules.json" file. This helps catch issues early and avoid runtime surprises. The validator:
• Confirms all name entries refer to actual methods (using GetMethod(...))
• Ensures match arrays are not empty
• Optionally checks for duplicate or overly generic rules (e.g., rules that match any input)
This step is run at app startup and helps ensure all routing rules are both valid and intentional.
Then:
- The request comes in as nested JSON
- The JSON is flattened into a dictionary with keys like patinfo.dob or ominvoice.invoice_no
- The dispatcher loads the validated rules from rules.json
- It checks each rule to see if all required fields are present and not null
- It picks the most specific matching rule
- It uses reflection to call the method named in the rule ("name")
Flattening the JSON input
You cannot easily check for field presence like "patinfo.dob" or "ominvoice.invoice_no" with a simple dictionary lookup because those values are nested inside JSON objects.
Flattening turns nested structures into a flat dictionary where the keys represent the full JSON path using dot notation, which makes things much easier.
Here is an example of a nested JSON request:
{
"patinfo": {
"dob": "1980-05-01",
"address": {
"zip": "12345"
}
},
"ominvoice": {
"invoice_no": "INV123"
}
}
And the dictionary (flattened) representation of the JSON input.
{
"patinfo.dob": "1980-05-01",
"patinfo.address.zip": "12345",
"ominvoice.invoice_no": "INV123"
}
Each key is a string path pointing to the original value in the nested JSON which is easier to check instead of writing complex null checks or traversal code.
Example Dispatcher Code
This sample code uses the flattened dictionary to check against the rules in "rules.json" and then calls the right method.
// Find the first rule that matches ALL of its required fields
var matchedRule = rules
// Keep only the rules where every required field in the rule's `Match` array:
// - Exists in the flattened input dictionary
// - Is not null
.Where(r => r.Match.All(field =>
flattenedInput.ContainsKey(field) && flattenedInput[field] != null))
// If multiple rules match, prefer the one with the most required fields
// (i.e., the most specific match)
.OrderByDescending(r => r.Match.Count)
// Return the first matching rule, or null if none match
.FirstOrDefault();
// If no rule matched, return a 400 Bad Request
if (matchedRule == null)
return BadRequest("No matching handler found");
// Use reflection to locate a method on this controller with the same name
// as the rule's `name` field. Assume all handlers are private instance methods.
var method = this.GetType().GetMethod(
matchedRule.Name,
BindingFlags.NonPublic | BindingFlags.Instance
);
// If the method doesn't exist (e.g., typo in rules.json), return error
if (method == null)
return BadRequest($"Handler method '{matchedRule.Name}' not found");
// Dynamically invoke the matched handler method and pass the `request` object
// This assumes all handler methods follow the signature:
// Task<IActionResult> MethodName(PatientRequest request)
var result = await (Task<IActionResult>)method.Invoke(
this,
new object[] { request }
);
// Return the handler’s result
return result;
And the matching handler methods look like this:
private async Task<IActionResult> HandleByPatientIdNum(PatientRequest request)
{
// code here
}
private async Task<IActionResult> HandleByDemographics(PatientRequest request)
{
// code here
}
private async Task<IActionResult> HandleByInvoice(PatientRequest request)
{
// code here
}
Why This Is Useful
• You can change the rules without touching the code.
• Helps keep routing logic out of the controller methods.
• Avoids giant if-else/switch blocks.
• Easy to scale by just adding new methods and rules.
Final Thoughts
This rule-based dispatch system has made it much easier for me to handle flexible JSON inputs without burying myself in if-then/switch conditions. By flattening the input to a dictionary and matching fields to rules, I let my code decide what to do based on the data. It's clean, adaptable, and easy to extend. If your API needs to handle many different input patterns, this could be a great way to go.
Note:
While externalizing (rules.json) routing rules offers flexibility, the rules themselves must be managed carefully to avoid becoming a new source of complexity.
Top comments (1)
This is mistagged.
#c
is only for C.