Summary
ReadFromJsonAsync\<List\<T>> fails because the API wraps the array inside a data property. The deserializer expects a root JSON array, so it throws a JsonException. The fix is to deserialize to a wrapper DTO that contains the list, then extract the list for the generic repository.
Root Cause
- The JSON payload is an object with members
{ data: [], message: ..., isSuccess: ... }. ReadFromJsonAsync<List<T>>tells System.Text.Json to treat the entire payload as a JSON array.- Because the root token is an object, the serializer cannot convert it to
List<T>and raisesJsonException.
Why This Happens in Real Systems
- APIs often return a standard envelope (status, message, pagination, etc.) for consistency.
- Generic repository helpers are written assuming a bare array response for simplicity.
- Developers forget to model the envelope, leading to mismatched expectations between client and server.
Real-World Impact
- Runtime failures in production when the first request hits the endpoint.
- Hidden bugs for junior developers because the exception is caught and turned into a generic error result.
- Increased latency: each failed call may trigger retries or fallback logic.
- Loss of telemetry: the true cause (envelope mismatch) is obscured by generic error messages.
Example or Code (if necessary and relevant)
// Envelope DTO matching the API response
public class ApiResponse
{
public T Data { get; set; }
public string? Message { get; set; }
public bool IsSuccess { get; set; }
}
// Updated generic repository method
public async Task<IDataResult<List>> GetListAsync(string endpoint,
Expression<Func>? filter = null)
{
try
{
using var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri(_baseUrl.Trim());
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", _token);
var response = await httpClient.GetAsync($"api/{endpoint}");
if (!response.IsSuccessStatusCode)
return new ErrorDataResult<List>(response.ReasonPhrase ?? "Api error");
// Deserialize to the envelope first
var apiResponse = await response.Content
.ReadFromJsonAsync<ApiResponse<List>>();
if (apiResponse?.Data == null)
return new ErrorDataResult<List>("Deserialization failed");
var data = apiResponse.Data;
if (filter != null)
{
var compiledFilter = filter.Compile();
data = data.Where(compiledFilter).ToList();
}
return new SuccessDataResult<List>(data);
}
catch (Exception ex)
{
return new ErrorDataResult<List>(ex.Message);
}
}
How Senior Engineers Fix It
- Model the envelope (
ApiResponse<T>) once and reuse it across all generic calls. - Encapsulate deserialization logic in a helper method to keep repository code clean.
- Add unit tests that feed the exact JSON payload and assert successful mapping.
- Log the raw response on first failure to surface envelope mismatches quickly.
- Prefer
HttpClientFactoryand typed clients to avoid creating a newHttpClientper request.
Why Juniors Miss It
- They often focus on the collection type (
List<T>) and overlook the surrounding JSON object. - Lack of experience with API envelope patterns leads to assuming a flat array response.
- Junior code bases may hard‑code deserialization targets instead of using a reusable wrapper, making the bug hard to spot.
- Insufficient unit testing of the HTTP layer hides the mismatch until runtime.