M老立
M老立

Reputation: 347

How to create data model of JSON with varying types

I'm calling an API which returns response as following

{
    "data": {
        "name": "John",
        "score": 51
    },
    "ret": 0
}

When an error occurs, the response changes to

{
    "data": "error message",
    "ret": 1
}

Notice the 'data' property varies from an object to a string.

Now I'm able to use JsonConverter to return different classes upon different types, the issue is the model to hold this response. i.e. If I use

public class MyResponse
{
    [JsonConverter(typeof(MyResponseType))]
    [JsonProperty(PropertyName = "data")]
    public MyResponseType Data { get; set; }

    [JsonProperty(PropertyName = "ret")]
    public int ReturnCode { get; set; }
}

MyResponseType can certainly hold an object, but can't be cast to a string.

I tried to use a generic type to hold the data

public class MyReponse<T>
{
    [JsonProperty(PropertyName = "data")]
    public T Data { get; set; }

    [JsonProperty(PropertyName = "ret")]
    public int ReturnCode { get; set; }
}

But here comes another problem, this MyReponse class is used by a service in .Net Core, where service is initialized by dependency injection at ConfigureServices step, which doesn't allow generic to be passed in. This is how this model is used in service and how the service is initialized

Service:

public class MyService<T> : IMyService {
    public bool someMethod() {
        ...
        var resp = JsonConvert.DeserializeObject<MyReponse<T>>(myResponse);
        ...
    }
}

At Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddTransient<IMyService, MyService>(); // Generics can't be passed in here
    ...
}

Any suggestion on how to create a proper data model that can handle this case?

Upvotes: 0

Views: 134

Answers (2)

Jes&#250;s L&#243;pez
Jes&#250;s L&#243;pez

Reputation: 9241

I would manage it at the api client level.

public class MyApiClientException: Exception
{
    public int ErrorCode { get; private set; }

    public MyApiClientException(string message, int errorCode): base(message)
    {
        this.ErrorCode = errorCode;
    }
}

public class MyApiClient
{
    private readonly HttpClient httpClient;
    public MyApiClient(HttpClient httpClient)
    {
        this.httpClient = httpClient;
    }

    public async Task<T> GetAsync<T>(string uri)
    {
        using (var response = await httpClient.GetAsync(uri))
        {
            var stringContent = await response.Content.ReadAsStringAsync();
            var jtoken = JToken.Parse(stringContent);
            int errorCode = (int)jtoken["ret"];
            var jdata = jtoken["data"];
            if (errorCode > 0)
            {
                throw new MyApiClientException((string)jdata, errorCode);
            }
            return jdata.ToObject<T>();
        }
    }
}

Then calling like this:

var data = await client.GetAsync<MyResponseType>("api/whatever");

Note that you no longer need JsonConverter. MyApiClient.GetAsync returns MyResponseType or whatever, it no longer returns the entire response MyResponse, because once you know ret = 0 which means no error, you don't need it anymore, just the data.

Regarding dependency injection, instead of making the service class generic MyService<T>, make it non generic and their methods generic.

Another option is providing a factory like this:

public class MyServiceFactory 
{
    public MyService<T> CreateService<T>() 
    {
      throw NotImplementedException();
    }

}

Upvotes: 0

devcrp
devcrp

Reputation: 1348

So assuming that the "ret" value is 1 when there's an error, the simplest solution I can think about is to check that property before casting.

And so you could have two models

public class MyResponse
{
    [JsonConverter(typeof(MyResponseType))]
    [JsonProperty(PropertyName = "data")]
    public MyResponseType Data { get; set; }

    [JsonProperty(PropertyName = "ret")]
    public int ReturnCode { get; set; }
}

and

public class MyErrorResponse
{
    [JsonProperty(PropertyName = "data")]
    public string Data { get; set; }

    [JsonProperty(PropertyName = "ret")]
    public int ReturnCode { get; set; }
}

And then decide to which type you should cast.

Upvotes: 2

Related Questions