2

I am getting the following from json payload from a REST request.

{
    "version": "1.1", 
    "date": "2017-01-06",
    "count": "130",
    "0": {
          "artist": "Artist 1",
          "title": "Title 1"
     },
    "1": {
          "artist": "Artist 2",
          "title": "Title 2"
     },
    ...
    "49": {
          "artist": "Artist 50",
          "title": "Title 50"
     }
}

As you can see, there are several numeric root elements. These elements index the object in the response, so I don't need the numbers themselves, I just need the Song object they represent. I also need count to know if I need to make a call to the server to get the next page of songs (the server returns 50 Song objects max per call).

Here is what I have for Song class:

public class Song
{
    public string artist {get; set;}
    public string title {get; set;}
}

But I am having issues getting the root element. I tried to mimic the process used when there is a dynamic child element, but this does not work.

public class SongsResponse
{
    public string version { get; set; }
    public string date { get; set; }
    public string count {get; set; }
    public Dictionary<string, Song> songs { get; set; }
}

I've seen solutions that "throw away" version, date and count and others that do it without pre-defined classes.

But I need count and ideally would want to work from predefined classes.

dbJones
  • 762
  • 1
  • 10
  • 31
  • Down vote with no comment?? – dbJones Jan 07 '17 at 00:29
  • Is refactoring an option? Why do you 'need' count? If `songs` implements `ICollection` it will have a `Count` property. – Jason Boyd Jan 07 '17 at 00:30
  • Server only returns 50 responses at a time. But `count` tells you how many fit the criteria. So `count` could be `250`. Updated question to make that clearer. – dbJones Jan 07 '17 at 00:32
  • So are the 'numbers as root elements' song ids or do they just represent the index of the song in the response body? – Jason Boyd Jan 07 '17 at 00:33
  • They merely index song within response body. – dbJones Jan 07 '17 at 00:34
  • The schema you are describing would not work in a data paging scenario unless the 'numbers as root elements' have more meaning than the index of the song within the response. Simply return 0 - 49 for every response does not give you enough information to determine that there are still more songs. They must represent a song id or the ordinal position of the song on the server. In which case I would expect the next response to be something like 50 - 99. – Jason Boyd Jan 07 '17 at 00:48
  • You would think that...but that's not what I'm getting from the sever. `page=2` also returns songs with indices 0-49 (even though they are 50 different songs than found on `page=1`). Very poorly designed json responses, in my opinion. – dbJones Jan 07 '17 at 00:52
  • 2
    Wow, that is awful! – Jason Boyd Jan 07 '17 at 00:55
  • Sounds like you want `[JsonTypedExtensionData]` and `[JsonConverter(typeof(TypedExtensionDataConverter))]` from [How to deserialize a child object with dynamic (numeric) key names?](http://stackoverflow.com/a/40094403/3744182). In fact, is that a duplicate? – dbc Jan 07 '17 at 02:34

2 Answers2

0

You do not need to include an index. The songs are indexed by virtue of the fact that they will be deserialized to a collection. Change your schema like so:

{  
   "version":"1.1",
   "date":"2017-01-06",
   "count":"130",
   "songs":[  
      {  
         "artist":"Artist 1",
         "title":"Title 1"
      },
      {  
         "artist":"Artist 2",
         "title":"Title 2"
      },
      {  
         "artist":"Artist 3",
         "title":"Title 3"
      }
   ]
}

public class Song
{
    public string artist {get; set;}
    public string title {get; set;}
}

public class SongsResponse
{
    public string version { get; set; }
    public string date { get; set; }
    public string count {get; set; }
    public List<Song> songs { get; set; }
}

Update

So you may have no control over the json being sent to you but you still have control over your local domain objects and how you deserialize the json. I would stick with the class definitions I defined above and create a custom JsonConverter that you can use to deserialize the response into something that makes a little more sense. Actually, while you are at it you can make your SongsResponse class 'better' by using the appropriate type for the date and count properties (why use a strongly typed language if you are not going to take advantage of it).

So this class:

public class SongsResponseJsonConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException("Unnecessary because CanWrite is false. The type will skip the converter.");
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var result = new SongsResponse();

        var obj = JObject.Load(reader);
        result.version = obj["version"].Value<string>();
        result.date = obj["date"].Value<DateTime>();
        result.count = obj["count"].Value<int>();
        result.songs =
            obj
            .Properties()
            .Where(x => !(new string[] { "version", "date", "count" }.Contains(x.Name)))
            .Select(x => new { Id = int.Parse(x.Name), Song = x })
            .OrderBy(x => x.Id)
            .Select(x => x.Song.Value.ToObject<Song>())
            .ToList();
        return result;
    }

    public override bool CanRead
    {
        get { return true; }
    }

    public override bool CanWrite
    {
        get { return false; }
    }

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

Which you would use like this:

var json = @"
{
    ""version"": ""1.1"", 
    ""date"": ""2017-01-06"",
    ""count"": ""130"",
    ""0"": {
        ""artist"": ""Artist 1"",
          ""title"": ""Title 1""
     },
    ""1"": {
        ""artist"": ""Artist 2"",
          ""title"": ""Title 2""
     },
    ""49"": {
        ""artist"": ""Artist 50"",
          ""title"": ""Title 50""
     }
}";

var response = JsonConvert.DeserializeObject<SongsResponse>(json, new SongsResponseJsonConverter());
Jason Boyd
  • 6,839
  • 4
  • 29
  • 47
  • Unfortunately, I can't change the schema. This is a response given to me by a server outside of my control. – dbJones Jan 07 '17 at 00:43
  • 3
    @JasonBoyd Your impression is wrong.. It is related with answer. `Change your schema like ....` is not answer. – L.B Jan 07 '17 at 00:52
  • @L.B Changing the schema is a perfectly valid answer if you have full control over it. It happens that the OP does not in this case but that was not apparent until after my original answer. – Jason Boyd Jan 07 '17 at 02:08
  • Now, Move all codes in *ReadJson* out of class and it will work too. So you don't need *JsonConverter* with this code... – L.B Jan 07 '17 at 13:13
0

It is possible to deserialize constant part of the JSON using common ways and then construct dynamic part usin JSON2LINQ. In your particular case it looks like the following:

var json = JObject.Parse(responseString);

var response = json.ToObject<SongsResponse>();
response.songs = json.Properties().Where(t => Regex.Match(t.Name, "\\d+").Success).ToDictionary(t => t.Name, t => t.Value.ToObject<Song>());

NOTE: Under the hood Regex.Match caches regex object after the first call and all subsequent calls reuse that object.

Yuriy Tseretyan
  • 1,676
  • 16
  • 17