2

I am going to do a test project with api-football.com. Their responses always follow the same shape - an example can be seen here. The Get, Parameters, Errors, Results and Response properties are always in the JSON, but the shape of "Response" depends on what you are calling. The other properties remain the same.

I was wondering how to model this in C#, and the idea hit me to use Generics to represent the Response property - that way, I could handle many different types of responses in a semi-elegant way.

So I made a base class like this:

public abstract class ApiFootballResponseBase<TResponse>
    where TResponse : IApiFootballResponseType
{
    public string Get { get; set; }

    [JsonConverter(typeof(ApiFootballDictOrEmptyArrayJsonConverter))]
    public Dictionary<string, string> Parameters { get; set; }

    [JsonConverter(typeof(ApiFootballDictOrEmptyArrayJsonConverter))]
    public Dictionary<string, string> Errors { get; set; }

    public ApiFootballPaging Paging { get; set; }

    public int Results { get; set; }

    public TResponse Response { get; set; }

    [JsonConstructor]
    public ApiFootballResponseBase()
    {
    }
}

IApiFootballResponseType is just an empty marker interface. The JsonConverter used with Parameters and Errors was written by me to handle the fact that these fields when empty are just set to an empty array ([]), while if they contain something, they are property bags that I want returned as dictionaries. The JsonConverter has been tested and works well enough.

We then have an example class I want to use to get the API status information from the URL above from.

public class ApiFootballStatusResponse : IApiFootballResponseType
{

    public ApiFootballSubscriptionInformation Subscription { get; set; }
    // TODO: Requests

    public ApiFootballAccountInformation Account { get; set; }

    [JsonConstructor]
    public ApiFootballStatusResponse()
    {
        
    }
}

These property types are also just simple property bags with [JsonConstructor] annotations on a parameterless constructor.

And the reason for that is the exception I get when I try to deserialize with RestSharp and System.Text.Json:

NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'ApiFootball.Responses.ApiFootballResponseBase`1[ApiFootball.Responses.ApiFootballStatusResponse]'. Path: $ | LineNumber: 0 | BytePositionInLine: 1.

However, as you can see, I have tried adding parameterless constructors to all classes, and marked them as JsonConstructor (the one from System.Text.Json, not NewtonSoft).

Is this exception really trying to tell me something else? Is this a bad way to try to handle the Response property with different contents depending on resource requested? If so, what would be a good way not to have to recreate the common properties of all requests in the different classes/records?

EDIT: In response to the request for more code from dbc in the comments, here is the (newly added) test code where I attempt to deserialize it myself, instead of leaving it to RestSharp:

    public async Task<ApiFootballResponseBase<ApiFootballStatusResponse>> GetStatus()
    {
        var request = new RestRequest("status", Method.Get);
        var result = await _client.GetAsync(request);
        
        var json = result.Content ?? "";

        return JsonSerializer.Deserialize<ApiFootballResponseBase<ApiFootballStatusResponse>>(json);
    }

I wrote it that way to make it easier to use the debugger. This gives me the same exception as above when it runs the return statement - NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'ApiFootball.Responses.ApiFootballResponseBase1[ApiFootball.Responses.ApiFootballStatusResponse]'`

If I can produce any further code to help see why this fails, let me know.

7
  • This question is similar to: Handling JSON single object and array. If you believe it’s different, please edit the question, make it clear how it’s different and/or how the answers on that question are not helpful for your problem. Commented Aug 5, 2024 at 20:28
  • It's not an exact duplicate of stackoverflow.com/questions/44100383/…, but I think the end result has enough overlap that it meets what you're looking for. Commented Aug 5, 2024 at 20:28
  • @gilliduck The primary difference between my question and theirs is probably that the question you link to is about Newtonsoft.json, while mine is concerning System.Text.Json. So while the two are often surprisingly similar, that MIGHT not be the case here. :) Commented Aug 6, 2024 at 4:17
  • @RuneJacobsen - could you please edit your question to share a minimal reproducible example that demonstrates the NotSupportedException when using System.Text.Json? The Deserialization of types without a parameterless constructor message can be misleading, for instance I believe STJ will generate that message when you try to deserialize an abstract type. So we need to see a minimal reproducible example that shows how that happens. Thanks! Commented Aug 7, 2024 at 20:14
  • Incidentally the System.Text.Json equivalent to Handling JSON single object and array is How to handle both a single item and an array for the same property using System.Text.Json?. Commented Aug 7, 2024 at 20:18

1 Answer 1

1

Is this exception really trying to tell me something else?

Yes. ApiFootballResponseBase<> is an abstract class, meaning it would need to be extended by some other class in order to be instantiable. Here's a very minimal reproduction that creates the same error message you're seeing:

JsonSerializer.Deserialize<Foo>("{}").Dump();

public abstract class Foo
{
}

"Public" constructors in abstract classes are effectively the same as protected constructors. In my opinion, the compiler should never have allowed you to declare a public constructor for an abstract class in the first place. While there is a code analyzer rule that can catch this, it's not enabled by default. Regardless, System.Text.Json should be more helpful in calling out when the class type you're instantiating is abstract, rather than focusing on the lack of a public constructor.

I'm guessing the abstract keyword was left over from some other things you were trying that involved extending a base class. Try removing the abstract keyword (and maybe remove "Base" from the class name while you're at it).

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

1 Comment

The abstract comes from trying to ensure that the base class can not be created by itself, which I guess would not be an issue as it has that type parameter anyway. This code is basically a spike to try to discover how to handle it. I was unaware of the situation with the constructors on the abstract class - thank you, it now works as expected!

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.