Introduction

ASP.NET Core model binding uses the [FromBody] attribute to deserialize JSON request bodies into action parameters. When the bound parameter is null despite a valid-looking JSON payload, the issue typically stems from missing or incorrect Content-Type headers, JSON property name mismatches, input formatter configuration, or the request body being read before model binding occurs. Diagnosing the root cause requires checking the HTTP request headers, JSON serializer settings, and middleware pipeline order.

Symptoms

  • Action parameter marked with [FromBody] is always null
  • ModelState.IsValid is true but the object has no data
  • Request body JSON looks correct but properties are not populated
  • Only specific endpoints have null binding while others work
  • Request works in Postman but fails from the frontend application

Debug model binding: ``csharp [HttpPost("users")] public IActionResult CreateUser([FromBody] UserDto user) { if (user == null) { // Log the raw request for debugging _logger.LogWarning("FromBody parameter is null. Content-Type: {ContentType}", Request.ContentType); return BadRequest("Invalid request body"); } // ... }

Common Causes

  • Missing or wrong Content-Type header (must be application/json)
  • JSON property names do not match C# property names (case sensitivity)
  • Request body already read by middleware (e.g., logging middleware)
  • Input formatter not configured for JSON
  • Model has no parameterless constructor or properties lack setters
  • JSON payload has syntax errors that cause silent deserialization failure

Step-by-Step Fix

  1. 1.**Ensure correct Content-Type header and valid JSON":
  2. 2.```csharp
  3. 3.// Client-side - fetch with correct headers
  4. 4.const response = await fetch('/api/users', {
  5. 5.method: 'POST',
  6. 6.headers: {
  7. 7.'Content-Type': 'application/json', // REQUIRED for [FromBody]
  8. 8.'Accept': 'application/json'
  9. 9.},
  10. 10.body: JSON.stringify({
  11. 11.firstName: "John",
  12. 12.lastName: "Doe",
  13. 13.email: "john@example.com"
  14. 14.})
  15. 15.});

// WRONG - no Content-Type header fetch('/api/users', { method: 'POST', body: JSON.stringify({ firstName: "John" }) // Missing Content-Type: application/json });

// WRONG - wrong Content-Type fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: JSON.stringify({ firstName: "John" }) }); ```

  1. 1.Configure JSON serialization options for property naming:
  2. 2.```csharp
  3. 3.builder.Services.AddControllers()
  4. 4..AddJsonOptions(options =>
  5. 5.{
  6. 6.// Use camelCase for JSON (default in .NET 6+)
  7. 7.options.JsonSerializerOptions.PropertyNamingPolicy
  8. 8.= JsonNamingPolicy.CamelCase;

// OR use exact property names (no policy) // options.JsonSerializerOptions.PropertyNamingPolicy = null;

// Ignore null values if desired // options.JsonSerializerOptions.DefaultIgnoreCondition // = JsonIgnoreCondition.WhenWritingNull; });

// DTO must have public setters or init-only properties public class UserDto { public string FirstName { get; set; } // camelCase: firstName public string LastName { get; set; } // camelCase: lastName public string Email { get; set; } // camelCase: email }

// For specific property overrides public class UserDto { [JsonPropertyName("first_name")] // Explicit name mapping public string FirstName { get; set; }

[JsonPropertyName("last_name")] public string LastName { get; set; } } ```

  1. 1.Enable request body buffering when reading body in middleware:
  2. 2.```csharp
  3. 3.// Middleware that reads the body BEFORE model binding
  4. 4.public class RequestLoggingMiddleware
  5. 5.{
  6. 6.private readonly RequestDelegate _next;

public async Task InvokeAsync(HttpContext context) { // Enable buffering so the body can be read again by model binding context.Request.EnableBuffering();

using var reader = new StreamReader( context.Request.Body, leaveOpen: true);

var body = await reader.ReadToEndAsync(); // Reset position for model binding to read again context.Request.Body.Position = 0;

await _next(context); } }

// Register before UseRouting in Program.cs app.UseMiddleware<RequestLoggingMiddleware>(); app.UseRouting(); app.UseAuthorization(); app.MapControllers(); ```

  1. 1.Verify model class is bindable:
  2. 2.```csharp
  3. 3.// CORRECT - has parameterless constructor and setters
  4. 4.public class UserDto
  5. 5.{
  6. 6.public int Id { get; set; }
  7. 7.public string Name { get; set; } = string.Empty;
  8. 8.}

// CORRECT - init-only properties (C# 9+) public record UserDto(int Id, string Name);

// WRONG - no setters, no init public class UserDto { public int Id { get; } // Read-only, cannot be set by model binding public string Name { get; } // Read-only }

// WRONG - requires constructor arguments with mismatched names public class UserDto { public UserDto(string fullName, int identifier) // Names don't match JSON { Name = fullName; Id = identifier; } public int Id { get; } public string Name { get; } } ```

Prevention

  • Always send Content-Type: application/json with JSON payloads
  • Use integration tests that send real HTTP requests to verify model binding
  • Log the raw request body in development when binding fails
  • Use [ApiController] attribute for automatic 400 responses on binding errors
  • Configure JSON options consistently across the application
  • Use record types with positional parameters for cleaner DTOs
  • Add a custom model binder or input formatter validator for complex scenarios