Situation
I am running into an issue with my implementation of .NET Core's System.Text.Json.JsonSerializer. The API which my application utilizes for getting data returns JSON in the following format:
{
"context": "SomeUnusedContextValue",
"value": [
{...}
]
}
I only care for the actual response, so I only need the value item.
I have written below method to get the specific property and deserialize the items into objects.
public static async Task<T?> DeserializeResponse<T>(Stream str, string? property, CancellationToken ct)
{
JsonDocument jsonDocument = await JsonDocument.ParseAsync(str, default, ct).ConfigureAwait(false);
if (property is null) // some calls to the API do return data at root-level
{
return JsonSerializer.Deserialize<T>(jsonDocument.RootElement.GetRawText());
}
if (!jsonDocument.RootElement.TryGetProperty(property, out JsonElement parsed))
throw new InvalidDataException($"The specified lookup property \"{property}\" could not be found.");
return JsonSerializer.Deserialize<T>(!typeof(IEnumerable).IsAssignableFrom(typeof(T))
? parsed.EnumerateArray().FirstOrDefault().GetRawText()
: parsed.GetRawText(), new JsonSerializerOptions
{ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault });
}
Problem
Now for my problem. Sometimes I only need a single object, however even if there is only one result, the API will still return an array. Not a problem since, as can be seen in the bottom return statement, I will just enumerate the array and get the first item (or the default null). This seems to crash when the returned array is empty, throwing below exception:
System.InvalidOperationException: Operation is not valid due to the current state of the object.
at System.Text.Json.JsonElement.GetRawText()
at BAS.Utilities.Deserializing.ResponseDeserializer.DeserializeResponse[T](Stream str, String property, CancellationToken ct) in C:\dev\bas.api\Modules\BAS.Utilities\Deserializing\ResponseDeserializer.cs:line 40
The object I'm trying to serialize into is as below:
public class JobFunctionCombination
{
/// <summary>
/// Gets or sets the combined identifier of the main function group, the function group and the sub function group.
/// </summary>
/// <example>01_0101_010101</example>
[JsonPropertyName("job_function_combination_id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string Id { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the combined names of the function groups.
/// </summary>
/// <example>Management | Human Resources | Finance controller</example>
[JsonPropertyName("description")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string Description { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the identifier of the main function group.
/// </summary>
[JsonPropertyName("job_main_function_group_id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string MainFunctionGroupId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the identifier of the function group.
/// </summary>
[JsonPropertyName("job_function_group_id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string FunctionGroupId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the identifier of the sub function group.
/// </summary>
[JsonPropertyName("job_sub_function_group_id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string SubFunctionGroupId { get; set; } = string.Empty;
}
The types and JsonPropertyName attributes all match to the returned JSON.
Attempted fixes
To try and fix this issue, I have tried some fixes (two of which you can still see in the given code samples).
- Adding the
JsonIgnoreattribute to the properties in the class.- I have tried to set the condition to
WhenWritingDefaultas well asWhenWritingNull. Neither seem to fix the problem.
- I have tried to set the condition to
- Setting the
DefaultIgnoreConditionin aJsonSerializerOptionsobject passed toJsonSerializer.Deserialze.- Here I have also tried both
WhenWritingDefaultandWhenWritingNull, also to no avail.
- Here I have also tried both
- Checking if the array in
JsonElement, when enumerated, is null or empty using.isNullOrEmpty().- This does prevent the exception from occuring, however it does not seem like an actual fix to me. More like a FlexTape solution just cancelling out the exception.
I am not sure what the exact issue is, other than the clear fact that JsonSerializer clearly has an issue with null objects. What could I do to fix this?
Your problem is with your call to
FirstOrDefault():JsonElementis astruct, so when the array has no items,FirstOrDefault()will return a default struct -- one initialized with zeros but without any property values set. Such an element does not correspond to any JSON token;ValueKindwill have the default value ofJsonValueKind.NoneandGetRawText()will have no raw text to return. In such a situation Microsoft chose to makeGetRawText()throw an exception rather than return a null string.To avoid the problem, enumerate the array, use a
Select()statement to deserialize each item, then afterwards doFirstOrDefault()to returndefault(T)when the array is empty, like so:Notes:
Since
Enumerable.Select()uses deferred execution, only the first element (if present) will be deserialized.In .NET 6 you may use
JsonSerialzier.Deserialize(JsonElement, Type, JsonSerializerOptions = default)to deserialize directly from aJsonElement. In .NET 5 and earlier, use one of theToObject<T>()workaround extension methods from System.Text.Json.JsonElement ToObject workaround.I corrected the logic that tests whether the type
Tis to be serialized as an array.JsonDocumentis disposable, and must be disposed of to return pooled memory to the memory pool.Demo fiddle here.