1

My application is consuming an API, and I'm trying to deserialize data of images coming back. The data is formatted like:

{
    "images":{
         "totalCount":4,
         "0":{
             "url":"file1.jpg"
         },
         "1":{
             "url":"file2.jpg"
         },
         "2":{
             "url":"file3.jpg"
        },
        "3":{
             "url":"file4.jpg"
        }
    }
}

I have these model classes:

public class MyViewModel
{
    [JsonProperty("images")]
    public ImagesViewModel Images { get; set; }
}

public class ImagesViewModel
{
    [JsonProperty("totalCount")]
    public int TotalCount { get; set; }

    public Dictionary<string, ImageViewModel> ListImages { get; set; }
}

public class ImageViewModel
{
    [JsonProperty("url")]
    public string Url { get; set; }
}

The collection of images isn't really a collection, for some reason it's just a new property for each image. I'm trying to deserialize my object like:

... // create HttpClient object, add headers and such
System.Net.Http.HttpResponseMessage response = await 
client.GetAsync(endpointUrl);
var jsonString = response.Content.ReadAsStringAsync();
MyViewModel model = 
    JsonConvert.DeserializeObject<MyViewModel>(jsonString.Result);

I get back the totalCount property just fine, but the collection of images is coming back null.

Is there a way for me to change my view models so that I can deserialize the json correctly?

Steven
  • 18,761
  • 70
  • 194
  • 296
  • Didn't you ask something similar to this yesterday. what makes this different – Nkosi Dec 31 '17 at 23:54
  • Are you in control of that API? the data being return is poorly designed. Hence all the formatting issues. – Nkosi Dec 31 '17 at 23:56
  • I'm not in control of the API, I have no control of how the data is formatted. – Steven Dec 31 '17 at 23:57
  • That sucks. That data is horrible. ah well. Taking a look at it now. – Nkosi Dec 31 '17 at 23:58
  • But you're right about it not being much different than the question I asked yesterday, I'm not sure why this one doesn't work, maybe it's that `totalCount` property? – Steven Jan 01 '18 at 00:12
  • It is exactly because of `totalCount`. Introducing that threw a wrench into the strongly typed design. If you ignore/remove that property from your model the rest should populate. Assuming the count is not paged, you can get the count from the resulting dictionary. – Nkosi Jan 01 '18 at 00:13
  • Is there a way to specifcially ignore `totalCount`? If I just change my view model to return a `Dictionary`, I get an exception saying it can't convert 4 to type ImageViewModel – Steven Jan 01 '18 at 01:00
  • Then in that case you will have to go the long route and deserialize it using JObjects. – Nkosi Jan 01 '18 at 01:15
  • Your `ImagesViewModel` has a mixture of known and unknown property names -- but the unknown properties have fixed schema. Thus you should be able to apply the `[JsonTypedExtensionData]` attribute and the `TypedExtensionDataConverter` converter from [How to deserialize a child object with dynamic (numeric) key names?](https://stackoverflow.com/a/40094403/3744182). (In fact this may be a duplicate.) – dbc Jan 01 '18 at 20:00

3 Answers3

3

Given the formatting of the JSON you will have to go the long route and try to deserialize it using JObjects

//using Newtonsoft.Json.Linq
var jObject = JObject.Parse(jsonString);
var images = jObject.Property("images").Value<JObject>(); ;
var viewModel = new MyViewModel {
    Images = new ImagesViewModel {
        TotalCount = images.Property("totalCount").Value<int>(),
        ListImages = images.Properties().Skip(1).ToDictionary(p => p.Name, p => p.Value<ImageViewModel>())
    }
};

Going a step further and using a JsonConverter for converting the payload itself actually works as well given that we know now how to convert it.

public class MyViewModelConverter : JsonConverter {

    public override bool CanConvert(Type objectType) {
        return objectType == typeof(MyViewModel);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
        var jObject = JObject.Load(reader);//<-- Note the use of Load() instead of Parse()
        var images = jObject.Property("images").Value<JObject>(); ;
        var model = new MyViewModel {
            Images = new ImagesViewModel {
                TotalCount = images.Property("totalCount").Value<int>(),
                ListImages = images.Properties().Skip(1).ToDictionary(p => p.Name, p => p.Value<ImageViewModel>())
            }
        };
        return model;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
        throw new NotImplementedException();
    }
}

and decorating the class itself

[JsonConverter(typeof(MyViewModelConverter))]
public class MyViewModel {
    [JsonProperty("images")]
    public ImagesViewModel Images { get; set; }
}

Deserialization is now as you intended to do before

var jsonString = await response.Content.ReadAsStringAsync();
MyViewModel model = JsonConvert.DeserializeObject<MyViewModel>(jsonString);
Nkosi
  • 235,767
  • 35
  • 427
  • 472
0

.NET Abhors dynamic types. They fly in the face of solid type checking at compile time. That being said, there is support for it:

As the example data is basically just a array of images, any collection could deal with this input.

If you can not even define the types umanbigiously (you might have a array of images and one of strings), the only way is ExpandoObject. It is designed specifically to deal with such cases. It is basically a List[string, object] with some Syntax Sugar, but it also does includes functions like Property Change Notifications.

Christopher
  • 9,634
  • 2
  • 17
  • 31
0

Sounds like a job for a custom converter!

A custom converter will let you supply your own logic for deserializing specific types. Newtonsoft uses the target class to figure out with type if expects to find in the json and call the appropriate converter.

    class ImagesViewModelConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(ImagesViewModel);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            assertToken(JsonToken.StartObject);

            var obj = new ImagesViewModel()
            {
                ListImages = new Dictionary<string, ImageViewModel>()
            };

            while (reader.Read() && reader.TokenType != JsonToken.EndObject)
            {
                assertToken(JsonToken.PropertyName);
                var propName = (string)reader.Value;
                if (propName.Equals(nameof(ImagesViewModel.TotalCount), StringComparison.InvariantCultureIgnoreCase))
                {
                    reader.Read();
                    assertToken(JsonToken.Integer);
                    obj.TotalCount = (int)((Int64)reader.Value);
                    continue;
                }
                reader.Read();
                var image = serializer.Deserialize<ImageViewModel>(reader); // you can still use normal json deseralization inside a converter

                obj.ListImages.Add(propName, image);
            }

            return obj;

            void assertToken(JsonToken token)
            {
                if (reader.TokenType != token)
                    throw new Exception(); // might wanna add detailed errors
            }
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException(); // implement if needed
        }
    }

And then:

        var settings = new JsonSerializerSettings()
        {
            Converters = new[] { new ImagesViewModelConverter() }
        };
        var obj = JsonConvert.DeserializeObject<MyViewModel>(jsonString, settings);
        });

You can even change classes to be easier to handle, given that they no longer need to match the json exactly. You can for example replace the dict with an array and have the converter fill it in order.

Ridiculous
  • 443
  • 4
  • 9