Calling an ASP.NET Core MVC action from the browser and getting JSON back is the foundation of every dynamic page — dropdowns that load on demand, live search, dashboards that refresh without a reload. This tutorial builds a JsonResult endpoint and calls it two ways: with the browser's built-in fetch API (no library needed — the modern default), and with jQuery $.ajax for projects that already carry jQuery.

The JsonResult action
A controller action that returns JsonResult serializes whatever you hand it and sets Content-Type: application/json:
public class CatalogController : Controller
{
public JsonResult Categories() =>
Json(new[]
{
new { id = 1, name = "Mobiles" },
new { id = 2, name = "Audio" },
new { id = 3, name = "Storage" },
});
}
Browse to /Catalog/Categories and you get:
[{"id":1,"name":"Mobiles"},{"id":2,"name":"Audio"},{"id":3,"name":"Storage"}]
Notes on the modern behavior:
- ASP.NET Core serializes with System.Text.Json and camelCases property names by default (
Name→name) — match that casing in your JavaScript. Json(...)is fine for MVC pages. For a real API surface, prefer typed actions (ActionResult<T>) in an[ApiController]— see returning different content types from action results.
Calling it with fetch (recommended)
fetch is built into every browser — no package, no <script> tag:
<ul id="category-list"></ul>
<script>
async function loadCategories() {
const response = await fetch('/catalog/categories');
if (!response.ok) {
console.error('Request failed:', response.status);
return;
}
const categories = await response.json();
const list = document.getElementById('category-list');
list.innerHTML = '';
for (const c of categories) {
const li = document.createElement('li');
li.textContent = c.name;
list.appendChild(li);
}
}
loadCategories();
</script>
Two habits worth keeping: check response.ok (fetch does not throw on a 404 or 500 — only on network failure), and build DOM nodes with textContent rather than string-concatenated HTML so user data can't inject markup.
POSTing JSON to an action
Sending data back is the same API with a method, header, and body:
public class SaveCategoryRequest
{
public string Name { get; set; } = string.Empty;
}
[HttpPost]
public JsonResult Save([FromBody] SaveCategoryRequest request) =>
Json(new { saved = true, name = request.Name });
const response = await fetch('/catalog/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Wearables' }),
});
const result = await response.json();
[FromBody] tells MVC to bind the JSON body to the model — more on that in
ASP.NET Core MVC model binding. If your page uses antiforgery tokens, send the token in a RequestVerificationToken header alongside.
The jQuery equivalent
Plenty of production apps still include jQuery, and the endpoint doesn't care who calls it:
$.ajax({
url: '/catalog/categories',
type: 'GET',
dataType: 'json',
success: function (categories) {
const list = $('#category-list').empty();
categories.forEach(c => list.append($('<li>').text(c.name)));
},
error: function (xhr) {
console.error('Request failed:', xhr.status);
}
});
If you're touching this code in an older project it works exactly as it always did. For new pages, prefer fetch — one less dependency, promises instead of callbacks, and the same code works in service workers and Node.
When something doesn't work
- 404 — route doesn't match; the default route maps
/catalog/categoriestoCatalogController.Categories(). Check MVC routing. - Empty/null model on POST — missing
Content-Type: application/jsonheader or no[FromBody]on the parameter. - Works in Postman, fails in the browser from another origin — that's CORS, not your JavaScript.
- Wrong property names — remember camelCase: the C#
Namearrives asname.
Comments (0)
No comments yet — be the first to share your thoughts.