3

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?

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
Pavlos
  • 993
  • 7
  • 23
  • @BrianRogers I've added an example of a possible other JSON response from a different API. Also included the `Guest` class, which is just a POCO. My idea was to make a contract resolver for each API and figure out the mapping to my predefined classes there and pass these onto the JsonConvert to deserialize. – Pavlos Dec 04 '18 at 11:31

2 Answers2

1

I think you are overengineering it and it would become a mess, when you need to support more and more formats and producers. Just imagine if you have 15 producers of events with different formats, how will your resolver look like?

What you need is to create one set of classes for your domain, which suits your domain and needs.

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; }       
}

public interface IEventProvider
{
    Event[] GetEvents();
}

Then create a set of classes for every external producer and map it to your domain classes, for example with AutoMapper profile or manually.

namespace YourCompany.EventProvider.Api1
{
    // just an example with json2sharp, use data annotations if you want
    public class Guest
    {
        public int guest_id { get; set; }
        public string first_name { get; set; }
        public string last_name { get; set; }
    }

    public class Participants
    {
        public List<Guest> guests { get; set; }
    }

    public class RootObject
    {
        public int event_id { get; set; }
        public string event_name { get; set; }
        public string start_date { get; set; }
        public string end_date { get; set; }
        public Participants participants { get; set; }
    }

    public class Api1EventProvider : IEventProvider
    {
        public Event[] GetEvents()
        {
           RootObject[] api1Response = GetFromApi();
           return _mapper.Map<RootObject[], Event[]>(api1Response);
        }
    }       
}

Yes, there will me more classes. But this code would be better, more readable and maintainable; you will spend less time to create it, than to create the resolver; and future developers will not cry every time a producer changes his API.
The code quality is not about creating less classes.

Yeldar Kurmangaliyev
  • 33,467
  • 12
  • 59
  • 101
  • Some poor wording on my part, my apologies. My initial idea was to make a resolver for every provider, not a single one. I did consider creating classes for each provider and mapping them, but didn't go that way for the reasons you mentioned. It remains of course the best alternative. – Pavlos Dec 04 '18 at 12:21
1

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

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300