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
JsonIgnore
attribute to the properties in the class.- I have tried to set the condition to
WhenWritingDefault
as well asWhenWritingNull
. Neither seem to fix the problem.
- I have tried to set the condition to
- Setting the
DefaultIgnoreCondition
in aJsonSerializerOptions
object passed toJsonSerializer.Deserialze
.- Here I have also tried both
WhenWritingDefault
andWhenWritingNull
, 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()
:JsonElement
is 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;ValueKind
will have the default value ofJsonValueKind.None
andGetRawText()
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
T
is to be serialized as an array.JsonDocument
is disposable, and must be disposed of to return pooled memory to the memory pool.Demo fiddle here.