[FromBody]

Bind a parameter from the JSON request body, know the 400 and 415 responses for bad bodies, and control empty bodies with EmptyBodyBehavior.

Signature
[FromBody] CreateOrder request  |  [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] Settings? settings
  • Added in .NET ASP.NET Core 1.0

Parameters

ParameterTypeDescription
$EmptyBodyBehavioroptionalEmptyBodyBehavior= EmptyBodyBehavior.DefaultDefault, Allow or Disallow. Default treats an empty body as an error unless the parameter is nullable or has a default value; Allow binds null; Disallow always rejects.

Return value

n/aTells model binding to deserialize the parameter from the request body with the input formatter (MVC) or System.Text.Json (minimal APIs) matching the Content-Type.

Errors

Two body parameters on one action fail at startup (MVC) or when the endpoint is built (minimal APIs, normally on the first request rather than at app start) with InvalidOperationException. At request time: 415 for an unsupported Content-Type, 400 for malformed JSON or a missing body.

Changelog

  • PHP ASP.NET Core 5.0Added EmptyBodyBehavior.
  • PHP ASP.NET Core 6.0Minimal APIs honour [FromBody]; complex parameters on POST, PUT and PATCH handlers are inferred from the body without it.

[FromBody] says one thing: read this parameter from the request body. In an MVC controller the input formatters pick a deserializer by Content-Type; with the defaults that is System.Text.Json for application/json, text/json and application/*+json. Minimal APIs read JSON directly. Inside [ApiController] and in minimal POST/PUT/PATCH handlers a complex parameter is already inferred from the body, so the attribute mostly appears where inference would pick something else or where you want to be explicit.

One body per request

The body is a forward-only stream, read once. An action may therefore have only one body parameter; two fail when the endpoint is built. Wrap several values in one request record. If middleware or a filter must also see the body, call Request.EnableBuffering() before anything reads it.

What clients get back

Request[ApiController]Minimal API
Content-Type not JSON415, problem details415
Malformed JSON, or "two" for an int400 with an error under the JSON path ($.quantity) plus a required error for the parameter400, no body by default
Empty body400 "A non-empty request body is required."400, unless the parameter is nullable
Valid JSON, failing [Range]400 validation problem400 with AddValidation(), otherwise accepted

The confusing message in the second row ("The request field is required" next to the real JSON error) appears because a failed deserialisation leaves the parameter null. Read the $. entry first.

Empty bodies

By default an empty body is an error unless the parameter is nullable (with nullable reference types enabled) or has a default value. EmptyBodyBehavior.Allow binds null explicitly, useful for optional settings payloads; Disallow rejects even for nullable parameters.

Production failure modes

  • Bodies above Kestrel's MaxRequestBodySize (30,000,000 bytes by default) are cut off with 413. Raise it per endpoint with [RequestSizeLimit] for imports, not globally.
  • Unknown JSON properties are ignored by default, so a misspelt "quantiy" silently binds 0. Turn on JsonUnmappedMemberHandling.Disallow for contracts where that matters.
  • [FromBody] on a GET action works on the server, but many proxies and HTTP clients drop GET bodies. Keep bodies to POST, PUT and PATCH.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Deserialize POST /api/orders into a validated CreateOrder record and return 415 to a client that forgot Content-Type: application/json
  • Accept an optional JSON settings body on a preview endpoint where an empty request means use the defaults
  • Turn a misspelt JSON property in a payment request into a 400 instead of a silent zero amount

Frequently asked questions

Why does a body error say "The order field is required"?

Deserialisation failed (for example a string where a number belongs), so the parameter stayed null and the implicit required check for a non-nullable parameter added a second error. The entry keyed by the JSON path, such as $.quantity, is the real cause.

Do I need [FromBody] in a minimal API?

Not for complex types on POST, PUT or PATCH handlers; they are inferred from the body. Add it to be explicit or when a type could also be inferred from services.

How do I read the raw body and still bind it?

Call HttpContext.Request.EnableBuffering() early (in middleware), read the stream, and set Position back to 0 so model binding can read it again.

More examples

One request record instead of two bodies

The body becomes one JSON object with <code>shipping</code> and <code>payment</code> properties. <code>AddValidation()</code> walks nested records, so a missing <code>shipping.city</code> is reported with its full path in the 400 response.

Example · csharp
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddValidation();
var app = builder.Build();

// Wrong: (Address shipping, PaymentDetails payment) would be two body parameters
// and fails when the endpoint is built. One record carries both.
app.MapPost("/checkout", ([FromBody] CheckoutRequest request) =>
    TypedResults.Accepted($"/checkout/{Guid.NewGuid():N}", request));

app.Run();

public record Address([Required] string Line1, [Required] string City, [Required, StringLength(2)] string Country);
public record PaymentDetails([Required] string Method, [Range(0.01, 50000)] decimal Amount);
public record CheckoutRequest([Required] Address Shipping, [Required] PaymentDetails Payment);

Reject unknown JSON properties

With the default settings <code>{"invoiceId":"INV-77","ammount":10}</code> binds with <code>Amount = 0</code> and no error. <code>Disallow</code> turns the typo into a 400. It also rejects fields that newer clients send to older servers, so enable it for internal contracts you control at both ends.

Example · csharp
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Controllers use MVC's JSON options; minimal APIs use HttpJsonOptions. Set both.
builder.Services.AddControllers().AddJsonOptions(o =>
    o.JsonSerializerOptions.UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow);
builder.Services.ConfigureHttpJsonOptions(o =>
    o.SerializerOptions.UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow);

var app = builder.Build();
app.MapControllers();
app.MapPost("/payments", ([FromBody] PaymentRequest payment) => TypedResults.Ok(payment));
app.Run();

public record PaymentRequest(string InvoiceId, decimal Amount, string Currency);

Raise the size limit for one import endpoint

<code>[RequestSizeLimit]</code> overrides <code>MaxRequestBodySize</code> for one endpoint. The whole body is still deserialised into memory, so a very large import is better streamed (read <code>Request.Body</code> with <code>JsonSerializer.DeserializeAsyncEnumerable</code>) than bound with <code>[FromBody]</code>.

Example · csharp
using Microsoft.AspNetCore.Mvc;

namespace Shop.Api;

public record ProductRow(string Sku, string Name, decimal Price);

[ApiController]
[Route("api/catalog")]
public class CatalogImportController : ControllerBase
{
    // 100 MB for this action only; everything else keeps Kestrel's 30,000,000-byte default.
    [HttpPost("import")]
    [RequestSizeLimit(100_000_000)]
    public IActionResult Import([FromBody] List<ProductRow> rows) =>
        Ok(new { imported = rows.Count });
}

Discussion

  • Be the first to comment on this lesson.