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.Jsonis 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! 🚀
Top comments (0)