Search code examples
c#jsonrestsharpyahoo-api

Deserializing Yahoo! Fantasy API player JSON


I am trying to deserialize JSON returned by the Yahoo! fantasy API, using this resource:

http://fantasysports.yahooapis.com/fantasy/v2/game/nfl/players;start=0;count=1?format=json

RestSharp is throwing the following exception when attempting to deserialize the Player object (which is an array of arrays):

Unable to cast object of type 'RestSharp.JsonArray' to type 'System.Collections.Generic.IDictionary`2[System.String,System.Object]'

If I exclude the Player property from the PlayerRoot class, the deserialization is successful (but of course lacking most of the useful information).

I'm using a hand-coded object model; the Paste Special-->Paste JSON as Classes action in Visual Studio does not work for the JSON shown below, but it does appear to be valid JSON.

Is there something wrong with my object model? My assumption is that something about the player element (the nested array) is blowing up the RestSharp JSON deserializer.

Client code

public void GetPlayers()
{
    int start = 0;
    int count = 1;
    var request =
        new RestRequest("game/{gameType}/players;start={start};count={count}",
                        Method.GET);
    request.AddUrlSegment("gameType", _gameType);
    request.AddUrlSegment("start", start.ToString());
    request.AddUrlSegment("count", count.ToString());

    // This extension adds "?format=json" to the query string
    request.AddJsonParam();

    var response = _client.Execute<FantasyModel>(request);
    var data = (FantasyModel)response.Data;
}

Model

public class FantasyModel
{
    public FantasyContent FantasyContent { get; set; }
}

public class FantasyContent
{
    [DeserializeAs(Name = "xml:lang")]
    public string Language { get; set; }

    [DeserializeAs(Name = "yahoo:uri")]
    public string YahooUri { get; set; }

    public string Time { get; set; }

    public string Copyright { get; set; }

    public string RefreshRate { get; set; }

    public List<Game> Game { get; set; }
}

public class Game
{
    public string GameKey { get; set; }

    public string GameId { get; set; }

    public string Name { get; set; }

    public string Code { get; set; }

    public string Type { get; set; }

    public string Url { get; set; }

    public string Season { get; set; }

    public StatCategories StatCategories { get; set; }

    public Players Players { get; set; }
}

public class Players
{
    [DeserializeAs(Name = "0")]
    public PlayerRoot PlayerRoot { get; set; }

    public int Count { get; set; }
}

public class PlayerRoot
{
    public PlayerRoot()
    {
        Player = new List<List<Player>>();
    }

    public List<List<Player>> Player { get; set; }
}

public class Player
{
    public Player()
    {
        Name = new PlayerName();
        ByeWeeks = new ByeWeeks();
        Headshot = new Headshot();
        EligiblePositions = new List<EligiblePosition>();
    }

    public string PlayerKey { get; set; }

    public string PlayerId { get; set; }

    public PlayerName Name { get; set; }

    public string EditorialPlayerKey { get; set; }

    public string EditorialTeamKey { get; set; }

    public string EditorialTeamFullName { get; set; }

    public string EditorialTeamAbbr { get; set; }

    public ByeWeeks ByeWeeks { get; set; }

    public string UniformNumber { get; set; }

    public string DisplayPosition { get; set; }

    public Headshot Headshot { get; set; }

    public string ImageUrl { get; set; }

    public string IsUndroppable { get; set; }

    public string PositionType { get; set; }

    public List<EligiblePosition> EligiblePositions { get; set; }
}

public class PlayerName
{
    public string Full { get; set; }

    public string First { get; set; }

    public string Last { get; set; }

    public string AsciiFirst { get; set; }

    public string AsciiLast { get; set; }
}

public class ByeWeeks
{
    public string Week { get; set; }
}

public class Headshot
{
    public string Url { get; set; }

    public string Size { get; set; }
}

public class EligiblePosition
{
    public string Position { get; set; }
}

JSON

{
    "fantasy_content": {
        "xml:lang": "en-US",
        "yahoo:uri": "\/fantasy\/v2\/game\/nfl\/players;count=1;start=0",
        "game": [
            {
                "game_key": "331",
                "game_id": "331",
                "name": "Football",
                "code": "nfl",
                "type": "full",
                "url": "http:\/\/football.fantasysports.yahoo.com\/f1",
                "season": "2014"
            },
            {
                "players": {
                    "0": {
                        "player": [
                            [
                                {
                                    "player_key": "331.p.8850"
                                },
                                {
                                    "player_id": "8850"
                                },
                                {
                                    "name": {
                                        "full": "Jamaal Charles",
                                        "first": "Jamaal",
                                        "last": "Charles",
                                        "ascii_first": "Jamaal",
                                        "ascii_last": "Charles"
                                    }
                                },
                                {
                                    "editorial_player_key": "nfl.p.8850"
                                },
                                {
                                    "editorial_team_key": "nfl.t.12"
                                },
                                {
                                    "editorial_team_full_name": "Kansas City Chiefs"
                                },
                                {
                                    "editorial_team_abbr": "KC"
                                },
                                {
                                    "bye_weeks": {
                                        "week": "6"
                                    }
                                },
                                {
                                    "uniform_number": "25"
                                },
                                {
                                    "display_position": "RB"
                                },
                                {
                                    "headshot": {
                                        "url": "http:\/\/l.yimg.com\/iu\/api\/res\/1.2\/084uD0cG9qCYdPSjJLX9.A--\/YXBwaWQ9eXZpZGVvO2NoPTg2MDtjcj0xO2N3PTY1OTtkeD0xO2R5PTE7Zmk9dWxjcm9wO2g9NjA7cT0xMDA7dz00Ng--\/http:\/\/l.yimg.com\/j\/assets\/i\/us\/sp\/v\/nfl\/players_l\/20120913\/8850.jpg",
                                        "size": "small"
                                    },
                                    "image_url": "http:\/\/l.yimg.com\/iu\/api\/res\/1.2\/084uD0cG9qCYdPSjJLX9.A--\/YXBwaWQ9eXZpZGVvO2NoPTg2MDtjcj0xO2N3PTY1OTtkeD0xO2R5PTE7Zmk9dWxjcm9wO2g9NjA7cT0xMDA7dz00Ng--\/http:\/\/l.yimg.com\/j\/assets\/i\/us\/sp\/v\/nfl\/players_l\/20120913\/8850.jpg"
                                },
                                {
                                    "is_undroppable": "0"
                                },
                                {
                                    "position_type": "O"
                                },
                                {
                                    "eligible_positions": [
                                        {
                                            "position": "RB"
                                        }
                                    ]
                                },
                                [ ],
                                [ ]
                            ]
                        ]
                    },
                    "count": 1
                }
            }
        ],
        "time": "215.82293510437ms",
        "copyright": "Data provided by Yahoo! and STATS, LLC",
        "refresh_rate": "60"
    }
}

One note: the "0" object in the JSON above would be repeated n times, game/{gameType}/players;start={start};count={count}, where n = {count}. The resulting JSON would look something like:

{
    "fantasy_content": {
        ...
                "players": {
                    "0": { }
                    "1": { }
                    "2": { }
                    ...

For the purposes of this question, I'm only bringing back one object ({count} = 1).

Edit

Upon further review, the player object should be List<List<object>> or object[][], as each attribute of a player is its own element within the array.

I'm still not sure how to deserialize the thing using the RestSharp deserializer, but I am able to get most of what I need by using Json.NET.


Solution

  • I was not able to deserialize this JSON using RestSharp's built in deserializer. I think that Yahoo is returning some fairly strange JSON here (the players object is essentially a dictionary of a list of lists of objects), and that strangeness is making automatic deserialization difficult.

    I solved this using Json.NET and a CustomCreationConverter. I do not particularly like this solution, and I think there's a better way to do it, but on a tight deadline this is what I came up with (error handling/logging/etc. removed for brevity):

    New Client Code

    var request =
        new RestRequest("game/{gameType}/players;start={start};count={count}", Method.GET);
    request.AddUrlSegment("gameType", _gameType);
    request.AddUrlSegment("start", start.ToString());
    request.AddUrlSegment("count", count.ToString());
    request.AddJsonParam();
    
    var response = _client.Execute(request);
    var json = JObject.Parse(response.Content);
    var playersJson = json["fantasy_content"]["game"][1]["players"];
    
    // Remove the count element
    playersJson.Last.Remove();
    
    var players = JsonConvert.DeserializeObject<Dictionary<string, Player>>(
        playersJson.ToString(), new JsonPlayerConverter());
    

    New Player class

    public class Player
    {
        public Player()
        {
            Name = new PlayerName();
            ByeWeeks = new ByeWeeks();
            Headshot = new Headshot();
            EligiblePositions = new List<EligiblePosition>();
        }
    
        [JsonProperty(PropertyName = "player_key")]
        public string PlayerKey { get; set; }
    
        [JsonProperty(PropertyName = "player_id")]
        public string PlayerId { get; set; }
    
        [JsonProperty(PropertyName = "name")]
        public PlayerName Name { get; set; }
    
        [JsonProperty(PropertyName = "editorial_player_key")]
        public string EditorialPlayerKey { get; set; }
    
        [JsonProperty(PropertyName = "editorial_team_key")]
        public string EditorialTeamKey { get; set; }
    
        [JsonProperty(PropertyName = "editorial_team_full_name")]
        public string EditorialTeamFullName { get; set; }
    
        [JsonProperty(PropertyName = "editorial_team_abbr")]
        public string EditorialTeamAbbr { get; set; }
    
        [JsonProperty(PropertyName = "bye_weeks")]
        public ByeWeeks ByeWeeks { get; set; }
    
        [JsonProperty(PropertyName = "uniform_number")]
        public string UniformNumber { get; set; }
    
        [JsonProperty(PropertyName = "display_position")]
        public string DisplayPosition { get; set; }
    
        [JsonProperty(PropertyName = "headshot")]
        public Headshot Headshot { get; set; }
    
        [JsonProperty(PropertyName = "image_url")]
        public string ImageUrl { get; set; }
    
        [JsonProperty(PropertyName = "is_undroppable")]
        public string IsUndroppable { get; set; }
    
        [JsonProperty(PropertyName = "position_type")]
        public string PositionType { get; set; }
    
        [JsonProperty(PropertyName = "eligible_positions")]
        public List<EligiblePosition> EligiblePositions { get; set; }
    }
    

    CustomCreationConverter

    public class JsonPlayerConverter : CustomCreationConverter<Player>
    {
        public override Player Create(Type objectType)
        {
            throw new NotImplementedException();
        }
    
        public Player Create(Type objectType, JObject obj)
        {
            var array = obj["player"][0];
            var content = array.Children<JObject>();
    
            var player = new Player();
            foreach (var prop in player.GetType().GetProperties())
            {
                var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
                var propName = ((JsonPropertyAttribute)attr).PropertyName;
                var jsonElement = content.FirstOrDefault(c => c.Properties()
                                        .Any(p => p.Name == propName));
                var value = jsonElement.GetValue(propName);
                var type = prop.PropertyType;
    
                if (type == typeof(string))
                {
                    prop.SetValue(player, (string)value, null);
                }
                else if (type  == typeof(PlayerName))
                {
                    var playerName = JsonConvert.DeserializeObject<PlayerName>(value.ToString());
                    prop.SetValue(player, (PlayerName)playerName, null);
                }
                else if (type == typeof(Headshot))
                {
                    var headshot = JsonConvert.DeserializeObject<Headshot>(value.ToString());
                    prop.SetValue(player, headshot, null);
                }
                else if (type == typeof(ByeWeeks))
                {
                    var byeWeeks = JsonConvert.DeserializeObject<ByeWeeks>(value.ToString());
                    prop.SetValue(player, byeWeeks, null);
                }
                else if (type == typeof(List<EligiblePosition>))
                {
                    var eligiblePositions = JsonConvert.DeserializeObject<List<EligiblePosition>>(value.ToString());
                    prop.SetValue(player, eligiblePositions, null);
                }
            }
            return player;
        }
    
        public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
        {
            var obj = JObject.Load(reader);
            var target = Create(objectType, obj);
            serializer.Populate(obj.CreateReader(), target);
    
            return target;
        }
    }