How to read problem details JSON using HttpClient in C#

By FoxLearn 1/21/2025 9:05:55 AM   30
Problem details (RFC7807) is a standardized format for error responses, which uses the `Content-Type` of `application/problem+json`, an appropriate HTTP status code (e.g., 400 – Bad Request), and a response body structured as follows:

For example:

{
    "type": "https://foxlearn.com/html/rfc7231#section-6.5.2",
    "title": "Invalid Payment Amount",
    "status": 422,
    "traceId": "abc123xyz456",
    "errors": {
        "Amount": [
            "Amount must be greater than zero."
        ]
    }
}

The example above is based on how ASP.NET Core typically returns model validation errors, using the ValidationProblemDetails class.

Here’s an example of how to make a request to an API using HttpClient, check the Content-Type header to ensure it follows the problem details format, and then retrieve the content as a string:

var response = await httpClient.PostAsync(requestUrl, jsonContent);

if (!response.IsSuccessStatusCode &&
    response.Content.Headers.ContentType?.MediaType == "application/problem+json")
{
    var problemDetailsJson = await response.Content.ReadAsStringAsync();

    // Process the problem details
}

The null-conditional operator (?.) is used with ContentType to avoid errors in case the Content-Type header is not set.

There are several ways to use the problem details once retrieved:

  • Log the error details.
  • Display the error to the user.
  • Deserialize the problem details JSON to:
    • Show specific information (like just the error messages).
    • Attempt to automatically resolve the issue based on the error details and retry the request (this is challenging but possible if the API provides machine-readable error messages).

Deserialize problem details JSON

First, Create a custom class for problem details:

public class PaymentProblemDetails : PaymentProblemDetailsWithErrors
{
	public int InternalErrorCode { get; set; }
}

public class PaymentProblemDetailsWithErrors
{
    public string Type { get; set; }
    public string Title { get; set; }
    public int Status { get; set; }
    public string TraceId { get; set; }
    public Dictionary<string, string[]> Errors { get; set; }
}

Use HttpClient to make a request and read the response:

var response = await httpClient.PostAsync(paymentUrl, paymentContent);

if (!response.IsSuccessStatusCode && response.Content.Headers.ContentType?.MediaType == "application/problem+json")
{
    var json = await response.Content.ReadAsStringAsync();

    var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); // cache and reuse this

    // Deserialize the JSON into the custom class
    var problemDetails = JsonConvert.DeserializeObject<PaymentProblemDetails>(json, jsonOptions);

    Console.WriteLine($"Status: {problemDetails.Status}");
    Console.WriteLine($"Title: {problemDetails.Title}");

    // Check for specific errors in the response
    if (problemDetails.Errors.ContainsKey("Amount"))
    {
        Console.WriteLine($"Amount error: {string.Join(", ", problemDetails.Errors["Amount"])}");
    }
}

In this example:

  • Custom Class (PaymentProblemDetails): This class mirrors the problem details structure returned by the API, with properties for Type, Title, Status, TraceId, and a dictionary for errors.
  • HttpClient Call: The PostAsync method is used to send a payment request. If the response's content type is application/problem+json, the JSON error details are read.
  • Error Deserialization: We use JsonConvert.DeserializeObject from Newtonsoft.Json to convert the problem details JSON into our custom PaymentProblemDetails class.
  • Output: The error details are printed to the console. In this case, it will show the error for the "Amount" field.

If the API response includes additional fields, such as internalErrorCode, you can handle them by either:

  1. Subclassing the ProblemDetails class to include extra properties.
  2. Using the [JsonExtensionData] attribute to collect all additional properties in a dictionary.

For example, using the JsonExtensionData attribute:

public class PaymentProblemDetailsWithErrors
{
    public string Type { get; set; }
    public string Title { get; set; }
    public int Status { get; set; }
    public string TraceId { get; set; }
    public Dictionary<string, string[]> Errors { get; set; }

    [JsonExtensionData]
    public Dictionary<string, object> ExtensionData { get; set; }
}

You can now deserialize the data and access the additional properties from the dictionary associated with the [JsonExtensionData] attribute:

var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
var problemDetails = JsonSerializer.Deserialize<PaymentProblemDetailsWithErrors>(json, jsonOptions);

if (problemDetails.ExtensionData.TryGetValue("internalErrorCode", out var internalErrorCode))
{
    Console.WriteLine($"Internal error code: {internalErrorCode}");
}

Output:

Internal error code:-1