Десериализация нетипизированного JSON поля на строготипизированный объект .NET с использованием Newtonsoft.Json
Встречали ли вы JSON объекты, поля которых не имеют строгой типизации, а точнее, тип которых может варьироваться от string
и numeric
до Object
? Если ответ положительный, то вы могли заметить, что красотой решения десериализации подобных объектов не блещут, однако, об одном из них я бы и хотел сегодня написать.
Задача
Внешний (неподконтрольный нам API) предоставляет следующий формат ответа, который нам требуется десериализовать на строготипизированный объект:
Ответ:
{
"contact":[
{
"type":"cell",
"value":{
"country":"7",
"city":"123",
"number":"4567890",
"formatted":"+71234567890"
}
},
{
"type":"email",
"value":"applicant@example.com"
},
{
"type":"vk_id",
"value": 12342341
}
]
}
Как вы могли заметить, значение поля value
объекта contact
не имеет строго определённого типа:
- Если тип контакта
cell
то тип значенияObject
; - Если тип контакта
email
то тип значенияstring
; - Возможны так же значения типа
int
иArray
;
В .NET нет возможности объявить тип, который будет единообразно хорошо и удобно поддерживать весь предполагаемый зоопарк значений, в связи с этим для каждого возможного варианта мы будем использовать собственный тип:
- Для телефона-
class
; - Для email-
string
; - Для идентификатора-
long
;
Решение с использованием CustomCreationConverter
Одним из возможных способов решения может быть написание собственного CustomCreationConverter
(наследник JsonConverter
), который отвечает непосредственно за создание объекта на этапе десериализации, generic типом у которого будет указан интерфейс IContact
. В этом случае на этапе десериализации мы получим контроль над выполнением и сможем реализовать создание экземпляра требуемого нам типа контакта основываясь на значении type
.
Делается это в 3 шага:
Модель
Для строгой типизации была объявлена следующая модель, состаящая из:
- root объекта -
Resume
; - интерфейса
IContact
на место которого будут подставляться конкретные реализации; - реализации контактов:
EmailContact
,PhoneContact
,VkContact
;
public class Resume {
public IList<IContact> Contacts { get; set; }
}
public interface IContact {
string Type { get; set; }
}
public class EmailContact : IContact {
public string Type { get; set; }
public string Value { get; set; }
}
public class PhoneContact : IContact {
public string Type { get; set; }
public PhoneContactValue Value { get; set; }
}
public class VkContact : IContact {
public string Type { get; set; }
public int Value { get; set; }
}
public class PhoneContactValue {
public string Code { get; set; }
public string Number { get; set; }
public int CountryId { get; set; }
}
Реализация CustomCreationConverter
Пример реализации указан далее:
public class ContactConverter : CustomCreationConverter<IContact> {
public override IContact Create(Type objectType) {
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
var jObject = JObject.Load(reader);
var target = CreateContact(jObject);
return target;
}
/// <summary>
/// Create concrete contact base on contactType value
/// </summary>
/// <param name="jObject"></param>
/// <returns></returns>
private IContact CreateContact(JObject jObject) {
var contactType = (string)jObject.Property("type");
var json = jObject.ToString();
switch (contactType) {
case "email": {
return DeserializeObject<EmailContact>(json);
}
case "cell": {
return DeserializeObject<PhoneContact>(json);
}
case "vk_id": {
return DeserializeObject<VkContact>(json);
}
default:
throw new NotSupportedException("Unexpected contactType: " + contactType);
}
}
private T DeserializeObject<T>(string json) => JsonConvert.DeserializeObject<T>(json);
}
Подключение
Теперь всё что осталось сделать, так это подключить реализованный ранее конвертер в настройках JsonSerializer
:
var resume = JsonConvert.DeserializeObject<Resume>(response, new ContactConverter());
Результат
В результате получаем строготипизиронное представление конкретных контактов с удобной возможностью дальнейшего расширение (на случай появления новых типов контактов потребуется лишь объявить модель и добавить новое значение type
в оператор switch
).
Исходники доступны на GitHubGist src.