Search code examples
c#jsonpropertiesjson.netnested-properties

Deserialize nested properties using Json.Net without using data annotations


I have an application that gets data from multiple API's. To minimize the amount of classes, I would need to map to every property. I have implemented a simple json.net ContractResolver. However, when I try to map a property to a child property I run into some trouble.

JSON format 1:

{
    "event_id": 123,
    "event_name": "event1",
    "start_date": "2018-11-30",
    "end_date": "2018-12-04",
    "participants": {
        "guests": [
            {
                "guest_id": 143,
                "first_name": "John",
                "last_name": "Smith",               
            },
            {
                "guest_id": 189,
                "first_name": "Bob",
                "last_name": "Duke",    
            }
        ]
    }
}

JSON format 2:

{
    "name": "event2",
    "from": "2017-05-05",
    "to": "2017-05-09",
    "city":"Some other city",
    "country":"US",
    "guests": [
        {
            "email":"jane@smith.com",
            "firstName":"Jane",
            "lastName":"Smith",
            "telephone":"1-369-81891"
        }
    ],
}

Here are my model classes:

public class Event
{
    public int EventId { get; set; }
    public string EventName { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public List<Guest> Guests { get; set; }
}

public class Guest
{
    public string GuestId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }       
}

And my resolver:

public class EventResolver : DefaultContractResolver
{
    private Dictionary<string,string> PropertyMappings { get; set; }

    public EventResolver()
    {
        this.PropertyMappings = new Dictionary<string, string>
        {
            {"EventId", "event_id"},
            {"StartDate", "start_date" },
            {"EndDate", "end_date" },
            {"EventName", "event_name" },
            {"Guests", "participants.guests"}
        };
    }

    protected override JsonContract CreateContract(Type objectType)
    {
        return base.CreateContract(objectType);
    }

    protected override string ResolvePropertyName(string propertyName)
    {
        var resolved = this.PropertyMappings.TryGetValue(propertyName, out var resolvedName);
        return (resolved) ? resolvedName : base.ResolvePropertyName(propertyName);
    }
}

I understand a path won't work instead of a property name. How could one go about this?


Solution

  • I don't think the resolver idea is going to work because you are remapping more than just property names -- you are also trying to deserialize into a class structure that doesn't always match the shape of JSON. This job is better suited for a set of JsonConverters.

    Here's the basic approach:

    1. Create one JsonConverter for each model class for which the JSON varies.
    2. Inside the ReadJson method load a JObject from the reader.
    3. Detect which format you have by looking for well-known property names which are always present for that format. For example, if you can rely on event_id always being present in the first format, that is a good way to detect it, because you know the second format does not have that property. You can base this check on the presence of multiple properties if needed; the key is just to use some combination that appears in only one format and no others. (Or if you know ahead of time which format to expect, you can simply parameterize the converters, i.e. pass a format flag in the constructor.)
    4. Once the format is known, populate the model from the JObject.

    For the Event model shown in your question, the converter might look something like this:

    public class EventConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(Event);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            Event evt = new Event();
            JObject obj = JObject.Load(reader);
            if (obj["event_id"] != null)
            {
                // JSON format #1
                evt.EventId = (int)obj["event_id"];
                evt.EventName = (string)obj["event_name"];
                evt.StartDate = (DateTime)obj["start_date"];
                evt.EndDate = (DateTime)obj["end_date"];
                evt.Guests = obj.SelectToken("participants.guests").ToObject<List<Guest>>(serializer);
            }
            else if (obj["name"] != null)
            {
                // JSON format #2
                evt.EventName = (string)obj["name"];
                evt.StartDate = (DateTime)obj["from"];
                evt.EndDate = (DateTime)obj["to"];
                evt.Guests = obj["guests"].ToObject<List<Guest>>(serializer);
            }
            else
            {
                throw new JsonException("Unknown format for Event");
            }
            return evt;
        }
    
        public override bool CanWrite
        {
            get { return false; }
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    

    Similarly for the Guest model, we might have this JsonConverter:

    public class GuestConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(Guest);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            Guest guest = new Guest();
            JObject obj = JObject.Load(reader);
            if (obj["guest_id"] != null)
            {
                // JSON format #1
                guest.GuestId = (string)obj["guest_id"];
                guest.FirstName = (string)obj["first_name"];
                guest.LastName = (string)obj["last_name"];
            }
            else if (obj["email"] != null)
            {
                // JSON format #2
                guest.FirstName = (string)obj["firstName"];
                guest.LastName = (string)obj["lastName"];
                guest.Email = (string)obj["email"];
            }
            else
            {
                throw new JsonException("Unknown format for Guest");
            }
            return guest;
        }
    
        public override bool CanWrite
        {
            get { return false; }
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    

    To use the converters, add them to the Converters collection of the JsonSerializerSettings object and pass the settings to DeserializeObject() like this:

    var settings = new JsonSerializerSettings
    {
        Converters = new List<JsonConverter> { new EventConverter(), new GuestConverter() }
    };
    
    var evt = JsonConvert.DeserializeObject<Event>(json, settings);
    

    Demo fiddle: https://dotnetfiddle.net/KI82KB