1

I have to manage a lot of API calls to a service and all the response messages have a common structure except the "data" field which varies according to the endpoint called and whether the call was successful or not. Looking for a way to smartly manage the whole situation I produced a satisfying solution with the use of generics and Mapster. This is a typical response message:

    {
       "type": "send",
       "datetime": "2022-02-21",
       "correlation_id": "dc659b16-0781-4e32-ae0d-fbe737ff3215",
       "data": {
        "id": 22,
        "description": "blue t-shirt with stripes",
        "category": "t-shirt",
        "size": "XL"
       }
    }

The data field is totally variable, sometimes it is a one-level structure, sometimes a multi-level structure, sometimes an array, sometimes a simple string and sometimes a null. Clearly the response messages are known and depends on the called endpoint so I know what to expect when I make the call but there is a case where the structure can still change. If the call is unsuccessful and there is an error, a 200 is still returned from the endpoint but the response is like this:

    {
       "type": "error",
       "datetime": "2022-02-21",
       "correlation_id": "dc659b16-0781-4e32-ae0d-fbe737ff3215",
       "data": {
        "id": 1522,
        "description": "product code not found",
       }
    }

Looking for an elegant and concise solution to manage all cases with a single method, I was able to find a solution: These are my models:

    public class Response<T> where T : class
    {
        public string type { get; set; }
        public T data { get; set; } = null;
        public Error Error { get; set; } = null;
        public bool IsError => Error != null;
        public string ErrorMessage => IsError ? $"An error occurred. Error code {Error.id} - {Error.description}" : "";
    }
    public class Customer
    {
        public int customerId { get; set; }
        public string name { get; set; }
    }
    public class Supplier
    {
        public int supplierId { get; set; }
        public string company { get; set; }
    }
    public class Error
    {
        public int id { get; set; }
        public string description { get; set; }
    }

And this is my function that manage all the deserializations:

    private static Response<T> GetData<T>(string json) where T : class
    {
        //Deserialize the json using dynamic as T so can receive any kind of data structure
        var resp = JsonConvert.DeserializeObject<Response<dynamic>>(json);
        var ret = resp.Adapt<Response<T>>();
    
        if (resp.type == "error")
        {
            //Adapt the dynamic to Error property
            ret.Error = ((object)resp.data).Adapt<Error>();
            ret.data = null;
        }
        return ret;
    }

So I call my function in this way:

    var customerData = GetData<Customer>("{\"type\":\"send\", \"data\": {\"id\":1, \"name\": \"John Ross\"}}");
    if (customerData.IsError)
        Console.WriteLine($"ERROR! {customerData.ErrorMessage}");
    else
        Console.WriteLine($"The response is OK. Customer name is {customerData.data.name}");

As you can see, the solution adopted is elegant and works very well. The only problem I haven't found solution is that Mapster.Adapt doesn't fail if I try to fit the wrong json to type T. So if I deserialize the json of the customer in the supplier class I would not notice the problem.

Is there a way in Mapster to know if the object I'm trying to adapt isn't compatible with the target type? So I could raise an exception and my procedure would be perfect.

Here is a repo with working example https://github.com/mmassari/MapDynamicWIthMapster

Thank you

  • 1
    Is there a reason you're deserializing to `Response` and then mapping to `Response`, rather than deserializing straight to `Response`? – Richard Deeming Feb 11 '22 at 15:58
  • yes because I don't know if the response will be T until i have deserialized to dynamic. For example if I call GetData but the API return me an error the T will be of type "Error" not customer. – Michele Massari Feb 11 '22 at 22:12

1 Answers1

0

The solution you are trying to work out might seem elegant, but in reality you could be introducing a code smell which might create hard to catch bugs in the future.

You might want to reconsider using the knowledge you already have about the endpoints to deserialize the response to their correct response types. This is good practice, and it is simple to understand and maintain for both yourself and future developers.

With regards to the problematic "product code not found" responses, you could add a validation step that deserializes the JSON response to an object of type JObject which you can safely query to check for existence of known error messages.

Here is a simple example:

private static void ValidateResponse(string json)
{
    var knownErrors = new List<string>()
    {
        "Product code not found",
        "Customer does not exist",
        "Other known errors here"
    };

    var jobj = JsonConvert.DeserializeObject<JObject>(json);
    var error = jobj.SelectToken("data.description");
    if (error != null)
    {
        var errorMessage = error.Value<string>();
        if (knownErrors.Contains(errorMessage))
        {
            throw new ApplicationException(errorMessage);
        }
    }
}

You may want to consider trimming and normalizing casing to avoid getting thrown off by small variations in the error messages.

andrerav
  • 404
  • 5
  • 14