Search code examples
c#jsonjson.netjson-deserialization

Json.NET: Different JSON schemas for a single property


I'm working with a third-party API that plays fast and loose with its returned JSON objects. In C#, I'm trying to set up Json.NET to deserialize these JSON objects to classes, but I've run into a hitch: sometimes the same property name will be used with several different schemas, depending on context.

Here's an example of the JSON data structure:

{
    "examples": [{
            "data": "String data",
            "type": "foo"
        }, {
            "data": {
                "name": "Complex data",
                "19": {
                    "owner": "Paarthurnax"
                }
            },
            "type": "complex"
        }, {
            "data": {
                "name": "Differently complex data",
                "21": {
                    "owner": "Winking Skeever"
                }
            },
            "type": "complex"
        }
    ]
}

Before discovering this inconsistency, I represented the first example with this class:

public class Example {
    [JsonProperty("data")]
    public string Data {get; set;}

    [JsonProperty("type"]
    public string DataType {get; set;}
}

# In the main method
Example deserializedObject = JsonConvert.DeserializeObject<Example>(stringData);

Now, there are two problems with this approach:

  1. The "data" key has two different property types. Sometimes it's a String and sometimes it's an object. I could create a custom class for the inner object, but then Json.NET would complain about the string version instead.
  2. When "data" is an object, its property names are inconsistent - note that the second data entry has a property called 19 and the third has one called 21. The structure of these objects is the same, but since their key names are different, I can't directly map them to classes.

The first issue is the more pressing one.

I know that I could solve the issue using JsonExtensionData if necessary, but I'm not sure if this is the best way, and it doesn't provide any compile-time safety in my application.

What is the best way for me to deserialize this JSON into a C# class?


Solution

  • You can use a JsonConverter. Here's a fully functioning example:

    public class Example
    {
        public string StringData { get; set; }
    
        public ComplexData ComplexData { get; set; }
    
        public string Type { get; set; }
    }
    
    public class ComplexData
    {
        public string Name { get; set; }
    
        [JsonProperty("19")]
        public Foo Nineteen { get; set; }
    
        [JsonProperty("21")]
        public Foo TwentyOne { get; set; }
    }
    
    public class Foo
    {
        public string Owner { get; set; }
    }
    
    public class FlexibleJsonConverter : JsonConverter
    {
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var examples = new List<Example>();
            var obj = JObject.Load(reader);
            foreach (var exampleJson in obj["examples"])
            {
                var example = new Example { Type = (string)exampleJson["type"] };
                if (example.Type == "complex")
                {
                    example.ComplexData = exampleJson["data"].ToObject<ComplexData>();
                }
                else
                {
                    example.StringData = (string)exampleJson["data"];
                }
    
                examples.Add(example);
            }
    
            return examples.ToArray();
        }
    
        public override bool CanConvert(Type objectType)
        {
            return objectType.IsAssignableFrom(typeof(Example[]));
        }
    }
    
    private static void Main()
    {
        var json = @"{
                        ""examples"": [{
                                ""data"": ""String data"",
                                ""type"": ""foo""
                            }, {
                                ""data"": {
                                    ""name"": ""Complex data"",
                                    ""19"": {
                                        ""owner"": ""Paarthurnax""
                                    }
                                },
                                ""type"": ""complex""
                            }, {
                                ""data"": {
                                    ""name"": ""Differently complex data"",
                                    ""21"": {
                                        ""owner"": ""Winking Skeever""
                                    }
                                },
                                ""type"": ""complex""
                            }
                        ]
                    }";
    
        var examples = JsonConvert.DeserializeObject<IEnumerable<Example>>(json, new FlexibleJsonConverter());
    
        foreach (var example in examples)
        {
            Console.WriteLine($"{example.Type}: {example.StringData ?? example.ComplexData.Nineteen?.Owner ?? example.ComplexData.TwentyOne.Owner}");
        }
    
        Console.ReadKey();
    }