All articles

AJAX Calls with JsonResult in ASP.NET Core MVC — fetch and jQuery

Return JSON from ASP.NET Core MVC actions and call them from the browser — the fetch API for modern pages, jQuery $.ajax for legacy ones, POSTing JSON with FromBody, and common pitfalls.

0 · log in to like, save & follow Share on LinkedIn Share on X

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.

cover

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 (Namename) — 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/categories to CatalogController.Categories(). Check MVC routing.
  • Empty/null model on POST — missing Content-Type: application/json header 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# Name arrives as name.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.