Десериализовать вложенные свойства с помощью Json.Net без использования аннотаций данных
У меня есть приложение, которое получает данные от нескольких API. Чтобы свести к минимуму количество классов, мне нужно сопоставить каждое свойство. Я реализовал простой json.net ContractResolver
, Однако, когда я пытаюсь сопоставить свойство с дочерним свойством, у меня возникают некоторые проблемы.
JSON формат 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 формат 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"
}
],
}
Вот мои модельные классы:
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 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);
}
}
Я понимаю, что путь не будет работать вместо имени свойства. Как можно это сделать?
2 ответа
Я не думаю, что идея решателя сработает, потому что вы переопределяете не только имена свойств - вы также пытаетесь десериализовать в структуру класса, которая не всегда соответствует форме JSON. Эта работа лучше подходит для набора JsonConverter
s.
Вот основной подход:
- Создай
JsonConverter
для каждого класса модели, для которого меняется JSON. - Внутри
ReadJson
метод загрузкиJObject
от читателя. - Определите, какой у вас формат, найдя известные имена свойств, которые всегда присутствуют в этом формате. Например, если вы можете положиться на
event_id
всегда присутствовать в первом формате, это хороший способ обнаружить его, потому что вы знаете, что второй формат не имеет этого свойства. Вы можете основывать эту проверку на наличии нескольких свойств, если это необходимо; ключ заключается в том, чтобы просто использовать некоторую комбинацию, которая появляется только в одном формате, а не в других. (Или, если вы заранее знаете, какой формат ожидать, вы можете просто параметризовать конвертеры, т.е. передать флаг формата в конструкторе.) - Когда формат известен, заполните модель из
JObject
,
Для Event
Модель показанная в вашем вопросе, конвертер может выглядеть примерно так:
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();
}
}
Аналогично для Guest
модель, мы могли бы иметь это 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();
}
}
Чтобы использовать конвертеры, добавьте их в Converters
коллекция JsonSerializerSettings
объект и передать настройки DeserializeObject()
как это:
var settings = new JsonSerializerSettings
{
Converters = new List<JsonConverter> { new EventConverter(), new GuestConverter() }
};
var evt = JsonConvert.DeserializeObject<Event>(json, settings);
Демонстрационная скрипка: https://dotnetfiddle.net/KI82KB
Я думаю, что вы переоцениваете это, и это станет беспорядком, когда вам потребуется поддерживать все больше и больше форматов и продюсеров. Представьте себе, если у вас есть 15 производителей событий в разных форматах, как будет выглядеть ваш резольвер?
Вам нужно создать один набор классов для вашего домена, который соответствует вашему домену и потребностям.
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();
}
Затем создайте набор классов для каждого внешнего производителя и сопоставьте его с классами вашего домена, например, с помощью профиля AutoMapper или вручную.
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);
}
}
}
Да, у меня будет больше классов. Но этот код был бы лучше, более читабельным и обслуживаемым; вы потратите меньше времени на его создание, чем на создание резольвера; и будущие разработчики не будут плакать каждый раз, когда продюсер меняет свой API.
Качество кода не сводится к созданию меньшего количества классов.