1

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#?

2
  • 1
    IJsonOnDeserialized + JsonExtensionDataAttribute ... then you may check if any extended property exists ... you may even throw an exception if such exists Commented Oct 10, 2022 at 14:12
  • Ahh that's a good shout. Thank you. Commented Oct 10, 2022 at 14:17

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

1 Comment

This works beautifully. I've created a base class for any POCO that will be returned from my API, the base class implements this interface and my generic type is constrained against this base class to enforce it's usage. Beautiful solution. Thank you.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.