Transitioning to JSON: Supporting Both Body and Query Parameters in ASP.NET Core


Recently, I was tasked with updating older POST endpoints to support [FromBody] input instead of the traditional [FromQuery] model. As part of this transition, I needed to support both [FromQuery] and [FromBody] inputs for backward compatibility until all clients can be updated. This approach allows existing clients to continue functioning without interruption, while new clients benefit from cleaner, structured JSON.
While this pattern is helpful for POST or PUT endpoints where the client is sending data, it is not intended for GET requests. In GET requests the input should remain as route or query parameters and not the request body.
Sounds simple, right? But in practice, ASP.NET Core’s model binding and validation pipeline is not designed to easily accept both [FromBody] and [FromQuery] inputs for the same model. You typically need to choose one source of input (not both).
In this write-up I go through a (relatively) reusable way to support both input models without introducing unnecessary complexity or breaking existing client functionality.
Objective
Allow my ASP.NET Web API POST/PUT endpoints to accept parameters either through:
• JSON body (using [FromBody])
• Query string (using [FromQuery])
The logic is:
• If the body exists and is valid then use it instead of query parameters.
• If the body is null or invalid then look at query parameters.
• If the query parameters are null or invalid then re-check body as fallback.
• If neither exist or are invalid then return a validation error.
The Problem
• If I decorate my action parameters with just [FromBody], then clients must send JSON. If they do not, they will see errors like “Unsupported Media Type” or model validation failures because the body is missing. I am doing this to help with migration so existing clients do not have to change right away and to support a smooth transition period.
• Mixing [FromBody] and [FromQuery] on the same action parameters does not work particularly well and could throw unexpected validation failures.
• I need to move to a JSON body input but fallback gracefully to query parameters if the body is missing or invalid, especially for existing clients.
• I want validation to “just work” with either input method.
My Solution is an Extension Method That Takes Care of Both Input Types
Here is how I approached it:
• I wrote a helper method that first tries to read and validate my model from the JSON body. A helper method is a fancy way of saying "code" (method) that I have in a in a class file. This helps to simplify and centralize logic that I might otherwise duplicate across multiple places.
• If that does not work (maybe because the client sent no body or the JSON is invalid) then it then tries to build and validate the model from query parameters instead.
• The plan is to end up with one valid model object but it is also possible that neither input works.
• I call this helper from my controller action. If it returns a valid model, preferably from the JSON body (which is the goal here) then I continue with my business logic. If it doesn’t then I return a validation error to the caller.
What Does That Look Like?
I added a new class file named HttpRequestExtensions.cs to my project.
The file contains an extension method for my ASP.NET controllers that helps seamlessly retrieve and validate a model from either the JSON request body ([FromBody]) or the URL query parameters ([FromQuery]). In a nut shell:
• Read and parse the JSON body first, validating the model if present.
• If the body is missing or invalid, it then attempts to build and validate the model from the query string parameters.
• It returns the first valid model it finds, or null if neither source is valid.
This simplifies my controller code by centralizing the logic for supporting both input styles, making migration smoother and keeping the endpoint implementation clean and consistent.
Here is what the HttpRequestExtensions.cs file extension method looks like.
// This static class provides an extension method for Controller to support input binding
// from either JSON body ([FromBody]) or query string ([FromQuery]).
public static class HttpRequestExtensions
{
// Generic method to try binding a model of type T from the body or query.
// T must be a class with a parameterless constructor.
public static async Task<T?> TryGetModelFromBodyOrQueryAsync<T>(this Controller controller) where T : class, new()
{
var request = controller.HttpContext.Request;
// --- STEP 1: Try to bind from JSON body if content exists and is of type application/json ---
if (request.ContentLength > 0 &&
request.ContentType?.Contains("application/json", StringComparison.OrdinalIgnoreCase) == true)
{
try
{
// Attempt to deserialize the JSON body into the model
var bodyModel = await request.ReadFromJsonAsync<T>();
// If deserialization is successful and model passes validation, return it
if (bodyModel != null && controller.TryValidateModel(bodyModel))
return bodyModel;
}
catch
{
// Ignore JSON parsing errors and fall through to try query string
}
}
// --- STEP 2: Fallback to building the model from query string parameters ---
var queryModel = new T(); // Create a new instance of T
var props = typeof(T).GetProperties(); // Reflectively get all public properties
foreach (var prop in props)
{
var key = prop.Name;
// Skip if the query string does not contain the key
if (!request.Query.ContainsKey(key)) continue;
var value = request.Query[key].ToString();
// Skip if value is empty or whitespace
if (string.IsNullOrWhiteSpace(value)) continue;
try
{
// Determine the correct type, handling nullable types
var targetType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
// Convert the string query value to the correct property type
var convertedValue = Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
// Assign the converted value to the property
prop.SetValue(queryModel, convertedValue);
}
catch
{
// Ignore conversion errors (e.g., invalid format); leave default value
}
}
// Return the query-bound model if it passes validation; otherwise return null
return controller.TryValidateModel(queryModel) ? queryModel : null;
}
}
How I use it in my controller
I replaced the existing model-binding code in my controller with this for each of the actions:
var model = await this.TryGetModelFromBodyOrQueryAsync<MyModelType>();
if (model == null)
return ValidationProblem(ModelState);
// Proceed with my normal business logic using the valid model
Why Is This Awesome?
• Clean & simple controller actions with no messy manual parameter parsing.
• Supports both old clients (query string) and new clients (JSON body).
• Graceful fallback logic. JSON body is preferred but will fall back to query parameters (or error out).
• It is reusable and generic. It works with any of my public property model classes that are defined in my Model.
Is This Method a Good Approach?
It depends. Supporting both [FromBody] and [FromQuery] is not ideal for long-term API design, but it serves as a practical and effective short-term solution when transitioning existing endpoints from query parameters to JSON body input without breaking existing clients
• Have clients relying on query parameters.
• Want a clean and reusable way to handle dual input methods (JSON and parameters).
• Want validation to work seamlessly for either input method (JSON and parameters).
• Want to avoid complex conditional code scattered in my controllers.
Pros of This Approach
• Backwards compatibility: Existing clients keep working while new clients use JSON.
• Future-proof: Existing clients can switch to JSON body without changing server logic.
• Fewer Breaking Changes: Avoids forcing clients to rewrite request logic.
• Clean controller code: Single line to get your model, no manual parameter parsing.
• Generic and reusable: Can be used with any defined model class.
• Graceful fallback: Prefers JSON body but falls back to query parameters if body is missing or invalid.
Cons of This Approach
• Increased complexity: The API endpoint is no longer truly RESTful in a pure sense because it supports two very different input mechanisms simultaneously.
• Not Idiomatic: ASP.NET Core expects one clear model-binding path (e.g., [FromBody] or [FromQuery]). Supporting both circumvents the framework’s intended design.
• Potential Maintenance Burden: basically supporting two versions of input in one endpoint.
• Validation Complexity: Now have to validate two sources and resolve conflicts cleanly.
• Performance overhead: Minor overhead in parsing and validation twice per request.
Is There a Better Way?
Of course, there always is.
• Separate endpoints: Keep old endpoints accepting query parameters for legacy, and new ones accepting JSON body, rather than mixing both in one action. Create one endpoint that supports query parameters (/v1/endpoint) and another that only accepts a JSON body (/v2/endpoint). This is clean, versioned, and explicit.
• Custom model binders: Implement a custom model binder that can elegantly handle both query and body input, but this requires more advanced ASP.NET Core expertise.
• API Gateway or Middleware: Use an API gateway or middleware that transforms query parameters into JSON bodies before the request reaches your controller.
• Deprecate Gradually: Add support for JSON body now, log usage, and phase out query parameter support over time and eventually simplify the codebase to expect only one model-binding source (JSON).
Is Supporting Both JSON Body and Query Parameters a NoNo?
• Although not a strict “NoNo,” it is generally discouraged for clarity and maintainability reasons.
• APIs should ideally be explicit and consistent in how they accept input, mixing sources could potentially confuse both clients and maintainers.
• During transitions or migrations supporting both input styles is a reasonable and often necessary compromise.
• Keep the logic simple and well documented if supporting both input methods.
• Ultimately, the best practice is to have a clear contract (either query parameters or JSON body).
Why I Chose to Do This
Transition Period During API Modernization I am modernizing older API endpoints that originally only accepted query parameters, but now need to support more expressive JSON payloads. Instead of requiring all clients to switch immediately, I am allowing both input methods temporarily to maintain backward compatibility. This approach ensures that existing clients keep working without forcing major changes on them.
Convenience for Testing and Prototyping When I am testing endpoints in Postman it is easier to pass a few parameters through the URL for quick tests. This is especially helpful during development when the payload is small or simple.
When to Avoid
• Building a new API from scratch.
• For clean consistent POST/PUT API contracts.
• Prioritize long-term maintainability over short-term flexibility.
Summary
How easy is it to implement? • This approach takes a bit of setup since it uses a helper method. • Other options might be trickier, like writing a custom model binder.
How good is it for backwards compatibility? • This is one of the biggest strengths of this method—it lets old clients keep working without changes. • Other approaches might need separate versioned endpoints.
How clear and maintainable is the code? • Mixing query and body input adds a little complexity. • Alternatives that use one clear input model are easier to read and maintain.
Does validation still work well? • Yep. FluentValidation works fine with this method. • Same goes for most alternatives.
Is this how ASP.NET Core is “supposed” to be used? • Not exactly. It bends the usual model binding rules a bit. • Alternatives tend to stick closer to how the framework was designed to work.
Final Thoughts
This approach helps me keep the API backwards compatible without cluttering up the controller logic. It is a practical middle ground while I migrate endpoints, especially since I still need to support some legacy clients for now.
Once I am ready to drop query parameters entirely, I can just update the action signatures and remove the fallback helper with no major refactoring needed.
Bottom Line
• In the short term, I am okay supporting both [FromQuery] and [FromBody]as I am doing this intentionally with clear fallback rules and proper validation.
• For the long term, the plan is to eventually fully migrate to [FromBody] because it is cleaner and aligns better with modern, RESTful API design. Eventually I will deprecate [FromQuery] for POST and PUT requests.
• As a transition strategy my goal is to move each existing endpoint to JSON body input when the time is right.
Subscribe to my newsletter
Read articles from Sean M. Drew Sr. directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
