2

I have the following JSON chunk (extracted from the full JSON in order to simplify the question):

 "statistics": [
  {
    "Strength": {
      "min": 16,
      "max": 20
    }
  },
  {
    "Dexterity": {
      "min": 16,
      "max": 20
    }
  }]

I'd like to deserialize the "statistics" array into a C# array of "Statistic" objects, but I can"t find how to do it... The key of each statistic object can be anything : "Strength","Luck","Dexterity" and so on, and each of those keys are unique.

The C# data object would be like this :

public class Container
{
    public Statistic[] Statistics { get; set; }
}

public class Statistic
{
    //Contains the name of the statistic
    public string Name { get; set; }
    public int Min { get; set; }
    public int Max { get; set; }
}

Maybe can I use some kind of polymorphism by removing the Name property and creating all possible classes of statistics, but it deafeats the adaptability of my code.

Thank you a lot.

Vasilievski
  • 773
  • 6
  • 13
  • Your JSON format is consistent with a `Dictionary` rather than a list. See e.g. [Deserializing JSON when key values are unknown](https://stackoverflow.com/q/24901076/3744182). But are the `Name` properties guaranteed to be unique? – dbc Apr 29 '20 at 15:48
  • They are, I add this remark in my question. I read the linked article and come back to you, thanks. – Vasilievski Apr 29 '20 at 15:49
  • Is your data model fixed? The arrays doesn't seem the best choice here. – StepTNT Apr 29 '20 at 15:53
  • @dbc, I'm not exactly an expert in JSON, but I think the structure in the article you mentionned is different. They do have a JSON dictonary with key values pairs, what I have is an array of objects. Maybe some settings allow to deserialize my array into a dictionary, but I don't know how to do it. – Vasilievski Apr 29 '20 at 15:55
  • @StepTNT, the model is coming from a third party, I can't change it. But yes, it is fixed. One string key plus the Min Max object. – Vasilievski Apr 29 '20 at 15:56
  • @Vasilievski - your data model has an array of objects, but the JSON does not. The JSON has an object `statistics` with variable property names whose values have a fixed schema. JSON serializers can automatically deserialize such JSON to a dictionary, see https://www.newtonsoft.com/json/help/html/DeserializeDictionary.htm. Thus what we are suggesting is that, since the `Name` properties are unique, you should change your data model to use a dictionary, and everything will just work. – dbc Apr 29 '20 at 16:41
  • If you insist on using a list instead of a dictionary you will need a custom `JsonConverter` to transform the list into a JSON object. See e.g. [How to persist an object-model to JSON and serialize a dictionary as its array of values?](https://stackoverflow.com/a/55767735/3744182) for an example of such a converter. – dbc Apr 29 '20 at 16:42
  • 1
    @dbc Actually, the JSON represents a `List>`, not a simple `Dictionary`. – Brian Rogers Apr 29 '20 at 16:49
  • @BrianRogers - ah you're right, sorry about that. – dbc Apr 29 '20 at 16:50

2 Answers2

2

You can handle this tricky JSON using a simple JsonConverter for your Statistic class like this:

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject jo = JObject.Load(reader);
        JProperty prop = jo.Properties().First();
        Statistic stat = new Statistic
        {
            Name = prop.Name,
            Min = (int)prop.Value["min"],
            Max = (int)prop.Value["max"]
        };
        return stat;
    }

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

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

To use it, add a [JsonConverter] attribute to the Statistic class and it should work as expected:

[JsonConverter(typeof(StatisticConverter))]
public class Statistic
{
    public string Name { get; set; }
    public int Min { get; set; }
    public int Max { get; set; }
}

Here is a working demo: https://dotnetfiddle.net/H7wR0g

If you cannot (or don't want to) add an attribute to the Statistic class, you can also use the converter by passing it to the DeserializeObject method like this:

var container = JsonConvert.DeserializeObject<Container>(json, new StatisticConverter());

Fiddle: https://dotnetfiddle.net/UTLzBk

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • If the model is coming from a library, can he add the attribute needed for the converter? That's why I didn't got for a converter-based solution, but I may be wrong :) – StepTNT Apr 29 '20 at 16:33
  • @StepTNT You can also use the converter by passing it to `JsonConvert.DeserializeObject()` if you are not able to add an attribute to the `Statistic` class – Brian Rogers Apr 29 '20 at 16:34
  • @StepTNT I've updated my answer to show the alternative approach. – Brian Rogers Apr 29 '20 at 16:41
  • This makes your solution better than mine then, I didn't know you could pass converters like that! – StepTNT Apr 29 '20 at 16:46
1

Given the provided JSON, we can't directly go for a Dictionary structure, this is because we have an array of objects where each object has the Statistic name as property name (while it should be a value for a property named Name)

To avoid dealing with custom converters, here's a quick (non production-ready!) solution that can give you a good starting point. The idea is to deserialize it as a dynamic, then extract the statistics object as a JArray and build your result structure from there.

Please note that I'm not checking anything for null, so this works as long as the input is well-formed (you need Newtonsoft.Json from NuGet).

using System;
using System.Linq;
using Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace StackOverflow
{
    internal class Program
    {
        private static readonly string _inputJson =
            @"
{
    ""statistics"": [
        {
            ""Strength"": {
                ""min"": 16,
                ""max"": 20
            }
        },
        {
            ""Dexterity"": {
                ""min"": 16,
                ""max"": 20
            }
        }]
}
";

        private static void Main(string[] args)
        {
            var tempObject = JsonConvert.DeserializeObject<dynamic>(_inputJson).statistics as JArray;

            var result = new Container
            {
                Statistics = tempObject.Select(obj =>
                {
                    var token = obj.First as JProperty;
                    var stats = token.Value;
                    return new Statistic
                    {
                        Name = token.Name,
                        Min = Convert.ToInt32((stats["min"] as JValue).Value),
                        Max = Convert.ToInt32((stats["max"] as JValue).Value)
                    };
                }).ToArray()
            };

            foreach (var stat in result.Statistics) Console.WriteLine($"{stat.Name} = ({stat.Min}, {stat.Max})");
        }
    }
}

namespace Models
{
    public class Container
    {
        public Statistic[] Statistics { get; set; }
    }

    public class Statistic
    {
        //Contains the name of the statistic
        public string Name { get; set; }
        public int Min { get; set; }
        public int Max { get; set; }
    }
}

This is the output I'm getting:

Strength = (16, 20)

Dexterity = (16, 20)

StepTNT
  • 3,867
  • 7
  • 41
  • 82