Technology Sep 02, 2026 · 4 min read

Modern JSON Handling in HttpClient: Ditch the Manual Serialization!

Hey developers! 👋 I see way too much code that looks like this: // 😱 The old way - please stop doing this! var json = JsonConvert.SerializeObject(myObject); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await client.PostAsync(url, content); var respon...

DE
DEV Community
by Nick
Modern JSON Handling in HttpClient: Ditch the Manual Serialization!

Hey developers! 👋

I see way too much code that looks like this:

// 😱 The old way - please stop doing this!
var json = JsonConvert.SerializeObject(myObject);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync(url, content);
var responseJson = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<MyType>(responseJson);

There's a MUCH better way. Let's modernize your HTTP + JSON code!

The System.Net.Http.Json Way

Since .NET 5, System.Net.Http.Json gives us beautiful extension methods:

// ✨ The modern way
var response = await client.PostAsJsonAsync(url, myObject);
var result = await response.Content.ReadFromJsonAsync<MyType>();

That's it. Two lines. No manual serialization, no StringContent, no encoding worries.

📦 Fun Fact: System.Net.Http.Json is included by default in .NET 5+. For .NET Standard 2.0 or .NET Core 3.1, just add the NuGet package!

All the Extension Methods

Sending JSON

// POST with JSON body
await client.PostAsJsonAsync("/api/users", newUser);
await client.PostAsJsonAsync("/api/users", newUser, cancellationToken);

// PUT with JSON body
await client.PutAsJsonAsync("/api/users/123", updatedUser);

// PATCH with JSON body  
await client.PatchAsJsonAsync("/api/users/123", patchDocument);

Reading JSON

// Read response body as JSON
var user = await response.Content.ReadFromJsonAsync<User>();

// One-liner: GET and deserialize
var users = await client.GetFromJsonAsync<List<User>>("/api/users");
var user = await client.GetFromJsonAsync<User>("/api/users/123");

With Cancellation

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

var user = await client.GetFromJsonAsync<User>(
    "/api/users/123", 
    cts.Token);

Custom Serialization Options

Need custom JSON settings? Pass JsonSerializerOptions:

var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    PropertyNameCaseInsensitive = true,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    Converters = { new JsonStringEnumConverter() }
};

// Use everywhere
var user = await client.GetFromJsonAsync<User>("/api/users/123", options);
await client.PostAsJsonAsync("/api/users", newUser, options);

Pro Tip: Configure Once

Create a shared options instance:

public static class JsonDefaults
{
    public static JsonSerializerOptions Api { get; } = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        PropertyNameCaseInsensitive = true,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
    };
}

// Usage
var user = await client.GetFromJsonAsync<User>(url, JsonDefaults.Api);

💡 Source Generators for AOT

Building for Native AOT or want faster serialization? Use source generators!

[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(List<User>))]
[JsonSerializable(typeof(CreateUserRequest))]
public partial class AppJsonContext : JsonSerializerContext { }
// Use the generated context
var user = await client.GetFromJsonAsync(
    "/api/users/123",
    AppJsonContext.Default.User);

await client.PostAsJsonAsync(
    "/api/users",
    newUser,
    AppJsonContext.Default.CreateUserRequest);

Performance: Source generators are ~2-3x faster than reflection-based serialization and support Native AOT compilation!

Error Handling Done Right

public async Task<User?> GetUserAsync(int id)
{
    var response = await _client.GetAsync($"/api/users/{id}");

    if (response.StatusCode == HttpStatusCode.NotFound)
        return null;

    if (!response.IsSuccessStatusCode)
    {
        var error = await response.Content.ReadFromJsonAsync<ApiError>();
        throw new ApiException(error?.Message ?? "Unknown error", response.StatusCode);
    }

    return await response.Content.ReadFromJsonAsync<User>();
}

Handling Empty Responses

public async Task<User?> GetUserSafeAsync(int id)
{
    var response = await _client.GetAsync($"/api/users/{id}");
    response.EnsureSuccessStatusCode();

    // Handle empty body gracefully
    if (response.Content.Headers.ContentLength == 0)
        return null;

    return await response.Content.ReadFromJsonAsync<User>();
}

Real-World Typed Client Example

public class UserApiClient : IUserApiClient
{
    private readonly HttpClient _client;
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        PropertyNameCaseInsensitive = true
    };

    public UserApiClient(HttpClient client)
    {
        _client = client;
    }

    public async Task<IReadOnlyList<User>> GetAllAsync(CancellationToken ct = default)
    {
        return await _client.GetFromJsonAsync<List<User>>(
            "/api/users", 
            JsonOptions, 
            ct) ?? [];
    }

    public async Task<User?> GetByIdAsync(int id, CancellationToken ct = default)
    {
        try
        {
            return await _client.GetFromJsonAsync<User>(
                $"/api/users/{id}", 
                JsonOptions, 
                ct);
        }
        catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
        {
            return null;
        }
    }

    public async Task<User> CreateAsync(CreateUserRequest request, CancellationToken ct = default)
    {
        var response = await _client.PostAsJsonAsync("/api/users", request, JsonOptions, ct);
        response.EnsureSuccessStatusCode();
        return (await response.Content.ReadFromJsonAsync<User>(JsonOptions, ct))!;
    }

    public async Task<bool> DeleteAsync(int id, CancellationToken ct = default)
    {
        var response = await _client.DeleteAsync($"/api/users/{id}", ct);
        return response.IsSuccessStatusCode;
    }
}

Migration Cheat Sheet

Old Way (Newtonsoft) New Way (System.Text.Json)
JsonConvert.SerializeObject(obj) JsonSerializer.Serialize(obj)
JsonConvert.DeserializeObject<T>(json) JsonSerializer.Deserialize<T>(json)
new StringContent(json, ...) PostAsJsonAsync(url, obj)
ReadAsStringAsync() + Deserialize ReadFromJsonAsync<T>()

Wrapping Up

Stop writing boilerplate serialization code. System.Net.Http.Json makes HTTP + JSON a breeze:

  • Cleaner code
  • Better performance (especially with source generators)
  • Built-in cancellation support
  • Native AOT compatible

Your future self will thank you for the cleaner code! 🎉

Happy serializing! 🚀

DE
Source

This article was originally published by DEV Community and written by Nick.

Read original article on DEV Community
Back to Discover

Reading List