Skip to content

Request bodies

Run the complete page example.

Creating a person, saving a form or uploading data usually means sending more than a few values in the URL. That data goes in the request body. Refit can turn your C# value into the JSON, text or form data the service expects.

Start with a JSON body below. Later sections cover streams, JSON Lines, buffering and compression when a service or a larger upload needs them.

Send data in the body

1. Register the JSON types. Use generated metadata so the same client can work with AOT. Metadata describes the JSON names and readers for a type. Refit's generated client and the JSON generator do separate jobs: one builds HTTP requests, and the other writes and reads JSON.

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(Person))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(Person[]))]
[JsonSerializable(typeof(List<Person>))]
internal sealed partial class SampleJsonContext : JsonSerializerContext;

Create one serializer and reuse its settings. The JSON guide shows the complete setup.

2. Describe the body format. Put Body on the one parameter that supplies the body. The form input appears below.

internal interface IBodyApi
{
    [Post("/body/json")]
    Task<Person> JsonAsync([Body] Person person);

    [Post("/body/text")]
    Task<Person> TextAsync([Body] string text);

    [Post("/body/quoted")]
    Task<Person> QuotedAsync([Body(BodySerializationMethod.Serialized)] string text);

    [Post("/body/stream")]
    Task<Person> StreamAsync([Body] Stream stream);

    [Post("/body/content")]
    Task<Person> ContentAsync([Body] HttpContent content);

    [Post("/body/form")]
    Task<Person> FormAsync([Body(BodySerializationMethod.UrlEncoded)] ContactForm form);

    [Post("/body/lines")]
    Task<Person> LinesAsync([Body(BodySerializationMethod.JsonLines)] IEnumerable<Person> people);

    [Post("/body/gzip")]
    Task<Person> GzipAsync([Body(
        BodySerializationMethod.Serialized,
        true,
        Compression = RequestCompression.GZip,
        CompressionLevel = CompressionLevel.Fastest)] Person person);
}

3. Describe form fields. AliasAs changes a field name. Multi repeats the key for each item. SerializeNull sends an empty value instead of omitting a null property.

internal sealed class ContactForm
{
    [AliasAs("name")]
    public string FullName { get; init; } = "Ada Lovelace";

    [Query(CollectionFormat.Multi)]
    public string[] Tags { get; init; } = ["math", "code"];

    [Query(SerializeNull = true)]
    public string? Note { get; init; }
}

4. Send the requests. host.Client is the shared HTTP client from the first-request example. Settings uses the generated JSON context above.

const int graceId = 2;
IBodyApi api = RestService.ForGenerated<IBodyApi>(host.Client, Settings);
Person saved = await api.JsonAsync(new(1, "Ada"));
await api.TextAsync("hello");
await api.QuotedAsync("quoted");

await using MemoryStream stream = new("stream text"u8.ToArray());
await api.StreamAsync(stream);
Console.WriteLine(stream.CanRead); // True: the caller still owns the stream.

using StringContent content = new("content text");
await api.ContentAsync(content);
await api.FormAsync(new());
await api.LinesAsync([new(1, "Ada"), new(graceId, "Grace")]);
await api.GzipAsync(new(1, "Ada"));
Console.WriteLine(saved.Name); // Ada

The source example checks the actual bytes received by each local route. It also decompresses the gzip body and checks the restored JSON. JSON Lines puts a newline between the two items. Refit does not add a trailing newline. See: the complete body example.

Choose a serialization method

BodySerializationMethodBehavior
Default = 0Passes HttpContent and streams through. Sends a string as plain text. Uses the configured serializer for other values.
Serialized = 3Uses the configured serializer, including for strings. A JSON string includes quotes.
UrlEncoded = 2Sends form key/value pairs. A dictionary or a generated property map supplies the fields.
JsonLines = 4Sends an enumerable as one serialized value per line. Register the element types with the JSON context.
Json = 1An obsolete name retained for compatibility. Use Serialized in new code.

Supplied HttpContent and streams also bypass serialization in the form and JSON Lines helpers. A form string is escaped as one whole string, so name=Ada is sent as name%3DAda. Use a dictionary or model to send separate form fields. A single non-enumerable JSON Lines value is wrapped as one item. Form property names can come from AliasAs or the configured serializer's naming rules. The key formatter applies when no explicit name exists. See query formatting for the related naming and value format APIs.

Buffering and serialization modes

BodyAttribute has four constructors: no arguments, buffered, serializationMethod, or both. SerializationMethod defaults to Default. Buffered is null unless you supplied a bool. Null follows RefitSettings.Buffered, whose default is false. True makes Refit load the content into a buffer before sending. False skips that extra step.

BodyAttribute inherited = new();
BodyAttribute buffered = new(true);
BodyAttribute serialized = new(BodySerializationMethod.Serialized);
BodyAttribute explicitPolicy = new(BodySerializationMethod.Serialized, false) { Compression = RequestCompression.Brotli, CompressionLevel = CompressionLevel.Fastest };
Console.WriteLine(inherited.Buffered is null); // True
Console.WriteLine(explicitPolicy.Buffered); // False

RefitSettings.RequestBodySerialization controls a different step: how the serializer creates JSON content.

RequestBodySerializationModeBehavior
Default = 0Uses the serializer's usual content method. System.Text.Json uses its async metadata path.
Buffered = 1Uses ISynchronousContentSerializer to write a complete byte buffer.
Streamed = 2Uses that interface to write into the outgoing stream without storing the whole body.

Buffered content can provide its length before sending. Streamed content usually cannot. Both synchronous modes can use generated fast-path writers when the JSON options allow them. If your serializer lacks that capability, the generated and reflection builders fall back to its normal ToHttpContent method. The source example verifies that fallback with a wrapper that exposes only IHttpContentSerializer and retains generated JSON metadata. See serializer capabilities. Choose mode from the three values above. The runnable example sends and checks the same JSON through each mode, then repeats the requests with the content-only capability wrapper.

RefitSettings settings = new(host.Settings.ContentSerializer) { RequestBodySerialization = mode };
IBodyApi api = RestService.ForGenerated<IBodyApi>(host.Client, settings);
Person result = await api.JsonAsync(new(1, "Ada"));
Console.WriteLine(result.Name); // Ada
ContentOnlySerializer limited = new((SystemTextJsonContentSerializer)host.Settings.ContentSerializer);
RefitSettings fallback = new(limited) { RequestBodySerialization = mode };
IBodyApi fallbackApi = RestService.ForGenerated<IBodyApi>(host.Client, fallback);
Person fallbackResult = await fallbackApi.JsonAsync(new(1, "Ada"));

Compression and ownership

BodyAttribute.Compression overrides RefitSettings.RequestCompression for one method. Default follows settings. None opts that body out of compression. CompressionLevel applies when the attribute explicitly selects a coding.

Gzip works on every Refit target. Brotli needs .NET 8 or later. Zstandard deliberately requires .NET 11; it is unavailable in these .NET 10 examples. Unsupported codings throw PlatformNotSupportedException when Refit builds the request.

RequestCompressionResult
Default = 0The attribute takes coding and level from settings. Settings set to Default do not compress.
None = 1No coding; an attribute can opt out of a settings-level coding.
GZip = 2Content-Encoding: gzip.
Brotli = 3Content-Encoding: br on .NET 8 and later.
Zstandard = 4Content-Encoding: zstd on .NET 11 and later.

These policies are declared on generated API methods. The local timeout constant is 25 milliseconds. The example also checks whole-string form escaping and a single JSON Lines value.

internal interface IBodyPolicyApi
{
    [Post("/body/buffered")]
    Task<Person> BufferedAsync([Body(true)] Person person);

    [Post("/body/none")]
    Task<Person> NoneAsync([Body(BodySerializationMethod.Serialized, false, Compression = RequestCompression.None)] Person person);

    [Post("/body/brotli")]
    Task<Person> BrotliAsync([Body(BodySerializationMethod.Serialized, Compression = RequestCompression.Brotli, CompressionLevel = CompressionLevel.Fastest)] Person person);

    [Post("/body/form-text")]
    Task<Person> FormTextAsync([Body(BodySerializationMethod.UrlEncoded)] string text);

    [Post("/body/one-line")]
    Task<Person> OneLineAsync([Body(BodySerializationMethod.JsonLines)] Person person);

    [Get("/child")]
    Task<Person> RootedAsync();

    [Get("child")]
    Task<Person> RelativeAsync();

    [Get("/body/timeout")]
    [Timeout(BodyPolicies.TimeoutMilliseconds)]
    Task<Person> TimeoutAsync();
}

RequestCompressionOptions exists on .NET 9 and later, with a public default constructor. Its nullable GZip and Brotli properties accept ZLibCompressionOptions and BrotliCompressionOptions. A nonnull options object for a coding overrides the compression level for that coding. A null property leaves that coding using its resolved level. Options remain settings-level choices even when the body attribute selects the coding.

RefitSettings settings = new(host.Settings.ContentSerializer)
{
    RequestCompression = RequestCompression.GZip,
    RequestCompressionLevel = CompressionLevel.Fastest,
    RequestCompressionOptions = new() { GZip = new(), Brotli = new() },
};
IBodyApi inherited = RestService.ForGenerated<IBodyApi>(host.Client, settings);
IBodyPolicyApi overrides = RestService.ForGenerated<IBodyPolicyApi>(host.Client, settings);
await inherited.JsonAsync(new(1, "Ada"));
await overrides.BrotliAsync(new(1, "Ada"));
await overrides.NoneAsync(new(1, "Ada"));

The local handler decompresses both gzip and Brotli and verifies the exact restored JSON. It also verifies that the None attribute sends no content-coding header.

Zstandard options on .NET 11

The .NET 11 build adds RequestCompressionOptions.Zstandard, accepting ZstandardCompressionOptions. This separate generated-client project exercises all three coding option properties and verifies the exact headers and decompressed bytes. AppendChecksum configures the Zstandard frame, while a null options property selects the level-based path.

RefitSettings settings = new(host.Settings.ContentSerializer)
{
    RequestCompressionLevel = CompressionLevel.Fastest,
    RequestCompressionOptions = new() { GZip = new(), Brotli = new(), Zstandard = new() { AppendChecksum = true } },
};
ICompressionApi api = RestService.ForGenerated<ICompressionApi>(host.Client, settings);

Here coding selects GZip, Brotli or Zstandard.

settings.RequestCompression = coding;
Person result = await api.PutAsync(new(1, "Ada"));
Console.WriteLine(result.Name); // Ada

Run the separate .NET 11 project when using Zstandard; the main .NET 10 examples verify that requesting it fails.

URI and per-call deadline policies

RefitLegacy = 0 preserves the base-address path and requires a leading slash on the method path. Rfc3986 = 1 uses HttpClient's URI merge rules. With a base address ending in /root/, /child resolves to /child in RFC mode, and child appends to become /root/child. A base address without its final slash treats the last segment as a file to replace. The local example checks the legacy prefix and both RFC forms.

RefitSettings legacy = new(host.Settings.ContentSerializer) { UrlResolution = UrlResolutionMode.RefitLegacy };
IBodyPolicyApi legacyApi = RestService.ForGenerated<IBodyPolicyApi>(host.Client, legacy);
await legacyApi.RootedAsync(); // /root/child

RefitSettings rfc = new(host.Settings.ContentSerializer) { UrlResolution = UrlResolutionMode.Rfc3986 };
IBodyPolicyApi rfcApi = RestService.ForGenerated<IBodyPolicyApi>(host.Client, rfc);

TimeoutAttribute(int milliseconds) exposes its value through the read-only Milliseconds property. A positive value applies a deadline to the effective cancellation token for the call. Zero and negative values disable that per-call deadline. It composes with the caller's token, HttpClient timeout and handler timeouts; the first cancellation takes effect. The local handler waits for cancellation, so this example verifies the deadline without a live server.

TimeoutAttribute timeout = new(TimeoutMilliseconds);
TimeoutAttribute disabled = new(0);
TimeoutAttribute negative = new(-1);
Console.WriteLine(timeout.Milliseconds);
IBodyPolicyApi api = RestService.ForGenerated<IBodyPolicyApi>(client, settings);
try
{
    await api.TimeoutAsync();
}
catch (OperationCanceledException)
{
    Console.WriteLine("The per-call deadline canceled the request.");
    return;
}

A timeout surfaces as OperationCanceledException or its TaskCanceledException subclass.

Refit keeps a supplied stream open when it disposes the request. You own and dispose that stream. A supplied HttpContent becomes request content and is disposed with the request. Do not share the same content instance across concurrent calls.

Obsolete JSON body method

BodySerializationMethod.Json = 1 is an obsolete compatibility value. It follows the serializer path, including for strings, while Serialized = 3 supplies the current name without a compiler warning. A compiler probe verifies the shipped CS0618 diagnostic; the live examples use Serialized. The probe names that diagnostic with the ObsoleteWarning constant.

const string source = """
    internal static class LegacyBodyMode
    {
        internal static Refit.BodySerializationMethod Mode => Refit.BodySerializationMethod.Json;
    }
    """;
CSharpCompilation compilation = ToolingCompilation.Create(source);
bool warned = AnalyzerSample.Contains(compilation.GetDiagnostics(), ObsoleteWarning);

Body creation and coding rules are in GeneratedRequestRunner.BodyContent.cs, GeneratedRequestRunner.cs and RequestContentCoding.cs. Per-call cancellation is in RequestExecutionHelpers.cs.

API reference

APIDescriptionParameters or valueReturns and behavior
BodyAttributeMarks one interface-method parameter as the HTTP request body.Applies to a parameter.Refit uses the parameter value as HttpContent, stream content, plain text, or serialized content according to its type and SerializationMethod.
BodySerializationMethodSelects how Refit turns a body value into HTTP content.Enum values below.Use with BodyAttribute to choose text, serialized, form, or JSON Lines content.
RequestBodySerializationModeSelects how Refit writes serialized JSON request content.Enum values below.Configure through RefitSettings.RequestBodySerialization.
RequestCompressionSelects the content coding applied to a request body.Enum values below.Configure a default in RefitSettings or override it on BodyAttribute.
RequestCompressionOptionsHolds optional compressor-specific settings that replace the resolved compression level for each coding.Available on .NET 9 and later.Assign it to RefitSettings.RequestCompressionOptions.
TimeoutAttributeApplies a per-call timeout to a Refit interface method.Applies to a method.A positive timeout cancels the request when it elapses.
BodySerializationMethod.Default = 0Uses Refit's standard body rules.0Passes HttpContent and streams through, sends strings as plain text, and uses the configured serializer for other values.
BodySerializationMethod.Json = 1Retains the former name for serialized content.1; obsolete.Uses the configured serializer, including for strings. Use Serialized in new code.
BodySerializationMethod.UrlEncoded = 2Writes form URL-encoded content.2A dictionary or object's fields supply form keys and values.
BodySerializationMethod.Serialized = 3Serializes every body value with the configured content serializer.3Strings use the serializer too, so a JSON string includes its quotes.
BodySerializationMethod.JsonLines = 4Writes newline-delimited JSON.4Serializes each enumerable item with the configured serializer and writes one item per line.
RequestBodySerializationMode.Default = 0Uses the serializer's asynchronous JSON-content path.0System.Text.Json uses its metadata-based path.
RequestBodySerializationMode.Buffered = 1Serializes JSON into a complete byte buffer before sending.1; requires ISynchronousContentSerializer.Sends ByteArrayContent with Content-Length; suited to small and medium bodies.
RequestBodySerializationMode.Streamed = 2Writes JSON through a Utf8JsonWriter to the request stream.2; requires ISynchronousContentSerializer.Bounds peak memory with pooled chunks and does not set Content-Length; suited to large uploads.
RequestCompression.Default = 0Inherits the coding from RefitSettings.RequestCompression.0Uses the settings coding and level.
RequestCompression.None = 1Disables compression for this body.1Sends no content coding even when settings choose one.
RequestCompression.GZip = 2Compresses the body with gzip.2; every Refit target.Sends Content-Encoding: gzip.
RequestCompression.Brotli = 3Compresses the body with Brotli.3; .NET 8 and later.Sends Content-Encoding: br.
RequestCompression.Zstandard = 4Compresses the body with Zstandard.4; .NET 11 and later.Sends Content-Encoding: zstd.
BodyAttribute()Creates a body parameter attribute without overrides.None.Uses SerializationMethod.Default and leaves Buffered unset so settings decide.
BodyAttribute(bool buffered)Creates a body parameter attribute with an explicit buffering policy.buffered: bool.Sets Buffered; serialization remains Default.
BodyAttribute(BodySerializationMethod serializationMethod, bool buffered)Creates a body parameter attribute with explicit serialization and buffering policies.serializationMethod: BodySerializationMethod; buffered: bool.Sets both properties.
BodyAttribute(BodySerializationMethod serializationMethod)Creates a body parameter attribute with an explicit serialization method.serializationMethod: BodySerializationMethod.Sets SerializationMethod and leaves Buffered unset so settings decide.
RequestCompressionOptions()Creates empty compressor-specific settings.None; .NET 9 and later.All coding option properties are null, so compression uses its resolved level.
TimeoutAttribute(int milliseconds)Creates a method timeout attribute.milliseconds: int.A positive value applies the per-call deadline; zero or a negative value disables it.
BodyAttribute.BufferedGets the per-body buffering override.Read-only bool?.null uses RefitSettings.Buffered; true buffers content before sending and false skips it.
BodyAttribute.SerializationMethodGets the selected body serialization method.Read-only BodySerializationMethod; default Default.Determines how ordinary body values become HTTP content.
BodyAttribute.CompressionGets or sets a method-level request content coding.Settable RequestCompression; default Default.Default follows settings, while None opts this body out of a settings-level coding.
BodyAttribute.CompressionLevelGets or sets the compression effort for an explicitly selected coding.Settable CompressionLevel; default Optimal.Refit reads it only when Compression names a coding; otherwise settings provide the level.
RequestCompressionOptions.GZipGets or sets gzip-specific compressor settings.Settable ZLibCompressionOptions?.A non-null value replaces the resolved level for gzip; null uses that level.
RequestCompressionOptions.BrotliGets or sets Brotli-specific compressor settings.Settable BrotliCompressionOptions?.A non-null value replaces the resolved level for Brotli; null uses that level.
RequestCompressionOptions.ZstandardGets or sets Zstandard-specific compressor settings.Settable ZstandardCompressionOptions?; .NET 11 and later.A non-null value replaces the resolved level for Zstandard; null uses that level.
TimeoutAttribute.MillisecondsGets the timeout supplied to TimeoutAttribute.Read-only int, in milliseconds.The effective request deadline exists only when the value is positive.