Home > Software engineering >  System.Text.Json.Deserialize to a generic type
System.Text.Json.Deserialize to a generic type

Time:10-10

I've inherited a codebase from another developer, who has since left the company. I'm working with a very generic class that handles web requests / responses. The original design behind the class is that any action verb (post, put, get, etc) will all be handled by the SendContent method.

Here's an example of the Post method, which I've abridged here for clarity:

public Task<Result<TResult?>> Post<TResult, TPayload>(string endpoint, RequestHeaders headers, TPayload payload,
    CancellationToken cancellationToken = default) where TResult : class
{
    var postJson = JsonSerializer.Serialize(payload);

    return SendContent<TResult?>(endpoint, HttpMethod.Post, headers,
        new StringContent(postJson, Encoding.UTF8, "application/json"),cancellationToken);
}

Here's an example of the SendContent method, again, abridged for clarity:

protected async Task<Result<TResult?>> SendContent<TResult>(string endpoint, HttpMethod httpMethod,
    RequestHeaders headers, HttpContent httpContent, CancellationToken cancellationToken) where TResult : class
{
    // httpRequestMessage created here.
    try
    {
        using var httpResponse =
            await _httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);

        var jsonString = await httpResponse.Content.ReadAsStringAsync();

        var result = JsonSerializer.Deserialize<TResult>(jsonString)

It's fine, it works.

The issue that I've recently found is what happens if the JSON returned doesn't match the schema of TResult in any way.

For example, say TResult is:

public class TestReturnObject
{
    public int Id { get; set; }

    public string Name { get; set; }
}

An instance of which would be serialized to:

{"Id":2,"Name":"Test"}

But say something were to be changed on the API side, either in error or on purpose, and what I get returned is actually this:

{"UniqueId":"b37ffcdb-36b0-4930-ae59-9ebaa2f4e996","IsUnknown":true}

In that instance:

var result = JsonSerializer.Deserialize<TResult>(jsonString)

The "result" object will be a new instance of TResult. All the properties will be null, because there's no match on any of the names / values.

I've looked at the System.Text.Json source starting here: https://github.com/dotnet/runtime/blob/f03470b9ef57df11db0040168e0a9776fd11bc6a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.String.cs#L47

It seems to me that the reason for this is that the code will instantiate the object and then map the properties. Obviously, at runtime, it has no idea the string isn't TestReturnObject.

Right now, I've got the calling code null checking various properties that are required. Is there a smarter way of handling it, or does the code just hit the limits of generics in C#?

CodePudding user response:

You may implement IJsonOnDeserialized in your model then catch any extra properties with JsonExtensionDataAttribute

You may even throw an exception after such exists (or just add boolean property which would say that JSON contained them)

public class TestReturnObject : IJsonOnDeserialized
{
    public int Id { get; set; }
    public string Name { get; set; }

    [JsonExtensionData]
    public IDictionary<string, object>? ExtraProperties { get; set; }

    void IJsonOnDeserialized.OnDeserialized()
    {
        if (ExtraProperties?.Count > 0)
        {
            throw new JsonException($"Contains extra properties: {string.Join(", ", ExtraProperties.Keys)}.");
        }
    }
}

here is working example

  • Related