C# – How to read problem details JSON with HttpClient

Problem details (RFC7807) is a standardized error response format that has a Content-Type of application/problem+json, an error response code (i.e. 400 – Bad Request), and has a response body that looks like this:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "0HMHOVKKV3MHN:00000002",
    "errors": {
        "Seats": [
            "Seats is out of range (0-10)"
        ]
    }
}
Code language: JSON / JSON with Comments (json)

This can be extended to include any number of properties. The example shown above comes from the default way ASP.NET Core returns model validation errors (using the ValidationProblemDetails class).

Here’s an example of making a request to an API with HttpClient, reading the Content-Type response header to verify that it’s in the problem details format, and then loading the content into 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();

	//use it
}
Code language: C# (cs)

Note: Use the null-conditional operator (ContentType?.) to guard against cases where Content-Type isn’t populated.

There are a few ways you can use the problem details:

  • Log it.
  • Show it to the user.
  • Deserialize the problem details JSON in order to:
    • Show specific parts to the user (like just the errors).
    • Try to automatically correct the problem based on the specific errors and retry the request. Note: Difficult, but not impossible. The API would need to return machine-readable error messages that could be coded against.

I’ll show examples of deserializing the problem details JSON.

Deserialize problem details JSON

First, add your own problem details class with properties that you want:

public class ProblemDetailsWithErrors
{
	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; }
}
Code language: C# (cs)

Note: This is basically equivalent to the ValidationProblemDetails class used in ASP.NET Core (without the Extensions property).

Now you can read in the problem details JSON (as shown above) and deserialize it to this problem details class. I’ll show examples below.

Deserialize with System.Text.Json

Here’s an example of using the built-in System.Text.Json to deserialize the JSON from a problem details error response:

using System.Text.Json;

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

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

	var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); //note: cache and reuse this
	var problemDetails = JsonSerializer.Deserialize<ProblemDetailsWithErrors>(json, jsonOptions);

	Console.WriteLine($"Has {problemDetails.Errors?.Count} error(s)");
}


Code language: C# (cs)

This outputs the following:

Has 1 error(s)Code language: plaintext (plaintext)

Deserialize with Newtonsoft

Here’s an example of using Newtonsoft with HttpClient to deserialize the problem details JSON:

using Newtonsoft.Json;

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

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

	var problemDetails = JsonConvert.DeserializeObject<ProblemDetailsWithErrors>(json);

	Console.WriteLine($"Has {problemDetails.Errors?.Count} error(s)");
}


Code language: C# (cs)

Notice there’s no need to specify options? Newtonsoft’s default settings work with camel-cased JSON just fine.

This outputs the following:

Has 1 error(s)Code language: plaintext (plaintext)

Why not use the built-in ProblemDetails / ValidationProblemDetails classes?

For returning problem detail responses, ASP.NET Core uses two classes: ProblemDetails and ValidationProblemDetails.

You may be wondering, why add your own problem details class instead of just using the built-in classes (by adding a reference to Microsoft.AspNetCore.Mvc)? Two reasons:

  • In my testing, I wasn’t able to get deserialization to work with these classes.
  • By adding your own model class, you eliminate the dependency on Microsoft.AspNetCore.Mvc.

Handling additional error info

Let’s say you’re working with an API that returns the problem details JSON and some endpoints include additional error info. For example, consider the following:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "0HMHOVKKV3MHN:00000003",
    "errors": {
        "Seats": [
            "Seats is out of range (0-10)"
        ]
    },
    "internalErrorCode": 1000
}
Code language: JSON / JSON with Comments (json)

Assuming you’re going to add your own problem details class, you have two options for handling additional error info.

Option 1 – Create a subclass with the additional properties

Subclass your own problem details class and add properties for any additional error info:

public class MovieProblemDetails : ProblemDetailsWithErrors
{
	public int InternalErrorCode { get; set; }
}

public class ProblemDetailsWithErrors
{
	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; }
}
Code language: C# (cs)

Now you can deserialize to this subclass:

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

var problemDetails = JsonSerializer.Deserialize<MovieProblemDetails>(json, jsonOptions);

Console.WriteLine($"Internal error code {problemDetails.InternalErrorCode}");
Code language: C# (cs)

This outputs:

Internal error code 1000Code language: plaintext (plaintext)

Option 2 – Use the [JsonExtensionData] attribute

Use the [JsonExtensionData] attribute with a dictionary to catch any unmatched, extra properties that aren’t part of the model class.

using System.Text.Json.Serialization;

public class ProblemDetailsWithErrors
{
	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; }
}
Code language: C# (cs)

Note: This is the same approach the built-in ProblemDetails class uses.

Now you can deserialize this and get the additional properties from the dictionary associated with the [JsonExtensionData] attribute:

var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); //note: cache and reuse this
var problemDetails = JsonSerializer.Deserialize<ProblemDetailsWithErrors>(json, jsonOptions);

if (problemDetails.ExtensionData.TryGetValue("internalErrorCode", out object internalErrorCode))
{
	Console.WriteLine($"Got internal error code from extension data: {internalErrorCode}");
}
Code language: C# (cs)

This outputs:

Got internal error code from extension data: -1Code language: plaintext (plaintext)