Кастинг интерфейсов для десериализации в JSON.NET
Я пытаюсь настроить читатель, который будет принимать объекты JSON с различных веб-сайтов (думаю, что информация выскабливается) и переводить их в объекты C#. В настоящее время я использую JSON.NET для процесса десериализации. Проблема, с которой я сталкиваюсь, заключается в том, что он не знает, как обрабатывать свойства уровня интерфейса в классе. Так что-то от природы:
public IThingy Thing
вызывает ошибку:
не удалось создать экземпляр типа IThingy. Тип интерфейс или абстрактный класс не может быть инстанцирован.
относительно важно, чтобы он был IThingy, а не Thingy, поскольку код, над которым я работаю, считается чувствительным, и модульное тестирование очень важно. Издевательство над объектами для атомарных тестовых сценариев невозможно с полноценными объектами, такими как Thingy. Они должны быть интерфейсом.
Я долго изучал JSON.NET ' s документация на некоторое время теперь, и вопросы, которые я мог бы найти на этом сайт, связанный с этим все из более чем год назад. Какая-нибудь помощь?
кроме того, если это имеет значение, мое приложение написано в .NET 4.0.
15 ответов:
@SamualDavis предоставил отличное решение в вопрос, который я подытожу здесь.
Если вам нужно десериализовать поток JSON в конкретный класс, который имеет свойства интерфейса, вы можете включить конкретные классы в качестве параметров конструктор класса! десериализатор NewtonSoft достаточно умен, чтобы понять, что ему нужно использовать эти конкретные классы для десериализации свойств.
здесь пример:
public class Visit : IVisit { /// <summary> /// This constructor is required for the JSON deserializer to be able /// to identify concrete classes to use when deserializing the interface properties. /// </summary> public Visit(MyLocation location, Guest guest) { Location = location; Guest = guest; } public long VisitId { get; set; } public ILocation Location { get; set; } public DateTime VisitDate { get; set; } public IGuest Guest { get; set; } }
(скопированный с этот вопрос)
в тех случаях, когда я не имел контроля над входящим JSON (и поэтому не могу гарантировать, что он включает свойство $type), я написал пользовательский конвертер, который просто позволяет явно указать конкретный тип:
public class Model { [JsonConverter(typeof(ConcreteTypeConverter<Something>))] public ISomething TheThing { get; set; } }
Это просто использует реализацию сериализатора по умолчанию от Json.Net при явном указании конкретного типа.
обзор доступен на этом блоге пост. Исходный код приведен ниже:
public class ConcreteTypeConverter<TConcrete> : JsonConverter { public override bool CanConvert(Type objectType) { //assume we can convert to anything for now return true; } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { //explicitly specify the concrete type we want to create return serializer.Deserialize<TConcrete>(reader); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { //use the default serialization - it works fine serializer.Serialize(writer, value); } }
чтобы включить десериализацию нескольких реализаций интерфейсов, вы можете использовать JsonConverter, но не через атрибут:
Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer(); serializer.Converters.Add(new DTOJsonConverter()); Interfaces.IEntity entity = serializer.Deserialize(jsonReader);
DTOJsonConverter отображает каждый интерфейс с конкретной реализацией:
class DTOJsonConverter : Newtonsoft.Json.JsonConverter { private static readonly string ISCALAR_FULLNAME = typeof(Interfaces.IScalar).FullName; private static readonly string IENTITY_FULLNAME = typeof(Interfaces.IEntity).FullName; public override bool CanConvert(Type objectType) { if (objectType.FullName == ISCALAR_FULLNAME || objectType.FullName == IENTITY_FULLNAME) { return true; } return false; } public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) { if (objectType.FullName == ISCALAR_FULLNAME) return serializer.Deserialize(reader, typeof(DTO.ClientScalar)); else if (objectType.FullName == IENTITY_FULLNAME) return serializer.Deserialize(reader, typeof(DTO.ClientEntity)); throw new NotSupportedException(string.Format("Type {0} unexpected.", objectType)); } public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) { serializer.Serialize(writer, value); } }
DTOJsonConverter требуется только для десериализатора. Процесс сериализации остается неизменным. Объекту Json не нужно встраивать имена конкретных типов.
этой так пост предлагает то же решение, один шаг далее с общим JsonConverter.
зачем использовать конвертер? Существует собственная функциональность в
Newtonsoft.Json
чтобы решить эту точную проблему:Set
TypeNameHandling
наJsonSerializerSettings
доTypeNameHandling.Auto
JsonConvert.SerializeObject( toSerialize, new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Auto });
Это поместит каждый тип в json, который не хранится как конкретный экземпляр типа, а как интерфейс или абстрактный класс.
Я сам проверял, и он работает как шарм, даже со списками.
источник и альтернативная ручная реализация:Код Внутри Блог
Я нашел это полезным. Ты тоже можешь.
Пример Использования
public class Parent { [JsonConverter(typeof(InterfaceConverter<IChildModel, ChildModel>))] IChildModel Child { get; set; } }
Пользовательский Конвертер Создания
public class InterfaceConverter<TInterface, TConcrete> : CustomCreationConverter<TInterface> where TConcrete : TInterface, new() { public override TInterface Create(Type objectType) { return new TConcrete(); } }
две вещи, которые вы можете попробовать:
реализовать модель try / parse:
public class Organisation { public string Name { get; set; } [JsonConverter(typeof(RichDudeConverter))] public IPerson Owner { get; set; } } public interface IPerson { string Name { get; set; } } public class Tycoon : IPerson { public string Name { get; set; } } public class Magnate : IPerson { public string Name { get; set; } public string IndustryName { get; set; } } public class Heir: IPerson { public string Name { get; set; } public IPerson Benefactor { get; set; } } public class RichDudeConverter : JsonConverter { public override bool CanConvert(Type objectType) { return (objectType == typeof(IPerson)); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { // pseudo-code object richDude = serializer.Deserialize<Heir>(reader); if (richDude == null) { richDude = serializer.Deserialize<Magnate>(reader); } if (richDude == null) { richDude = serializer.Deserialize<Tycoon>(reader); } return richDude; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { // Left as an exercise to the reader :) throw new NotImplementedException(); } }
или, если вы можете сделать это в своей объектной модели, реализуйте конкретный базовый класс между IPerson и вашими конечными объектами и десериализуйте его.
первый потенциально может завершиться неудачей во время выполнения, второй требует изменений в вашей объектной модели и гомогенизирует вывод до самого низкого общего знаменателя.
для тех, кто может быть интересно о ConcreteListTypeConverter, на который ссылался Оливер, вот моя попытка:
public class ConcreteListTypeConverter<TInterface, TImplementation> : JsonConverter where TImplementation : TInterface { public override bool CanConvert(Type objectType) { return true; } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var res = serializer.Deserialize<List<TImplementation>>(reader); return res.ConvertAll(x => (TInterface) x); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { serializer.Serialize(writer, value); } }
предположим, что параметр autofac следующим образом:
public class AutofacContractResolver : DefaultContractResolver { private readonly IContainer _container; public AutofacContractResolver(IContainer container) { _container = container; } protected override JsonObjectContract CreateObjectContract(Type objectType) { JsonObjectContract contract = base.CreateObjectContract(objectType); // use Autofac to create types that have been registered with it if (_container.IsRegistered(objectType)) { contract.DefaultCreator = () => _container.Resolve(objectType); } return contract; } }
тогда предположим, что ваш класс выглядит так:
public class TaskController { private readonly ITaskRepository _repository; private readonly ILogger _logger; public TaskController(ITaskRepository repository, ILogger logger) { _repository = repository; _logger = logger; } public ITaskRepository Repository { get { return _repository; } } public ILogger Logger { get { return _logger; } } }
поэтому использование преобразователя в десериализации может быть следующим:
ContainerBuilder builder = new ContainerBuilder(); builder.RegisterType<TaskRepository>().As<ITaskRepository>(); builder.RegisterType<TaskController>(); builder.Register(c => new LogService(new DateTime(2000, 12, 12))).As<ILogger>(); IContainer container = builder.Build(); AutofacContractResolver contractResolver = new AutofacContractResolver(container); string json = @"{ 'Logger': { 'Level':'Debug' } }"; // ITaskRespository and ILogger constructor parameters are injected by Autofac TaskController controller = JsonConvert.DeserializeObject<TaskController>(json, new JsonSerializerSettings { ContractResolver = contractResolver }); Console.WriteLine(controller.Repository.GetType().Name);
вы можете увидеть больше деталей внутри http://www.newtonsoft.com/json/help/html/DeserializeWithDependencyInjection.htm
используйте этот класс для сопоставления абстрактного типа с реальным типом:
public class AbstractConverter<TReal, TAbstract> : JsonConverter { public override Boolean CanConvert(Type objectType) => objectType == typeof(TAbstract); public override Object ReadJson(JsonReader reader, Type type, Object value, JsonSerializer jser) => jser.Deserialize<TReal>(reader); public override void WriteJson(JsonWriter writer, Object value, JsonSerializer jser) => jser.Serialize(writer, value); }
...и когда десериализуют:
var settings = new JsonSerializerSettings { Converters = { new AbstractConverter<Thing, IThingy>(), new AbstractConverter<Thing2, IThingy2>() }, }; JsonConvert.DeserializeObject(json, type, settings);
для чего это стоит, в итоге мне пришлось справиться с этим сам по большей части. Каждый объект имеет десериализовать (строка jsonStream) метод. Несколько фрагментов из него:
JObject parsedJson = this.ParseJson(jsonStream); object thingyObjectJson = (object)parsedJson["thing"]; this.Thing = new Thingy(Convert.ToString(thingyObjectJson));
в этом случае новая фишечка(строка) - это конструктор, который вызовет десериализовать (строка jsonStream) метод соответствующего конкретного типа. Эта схема будет продолжать идти вниз и вниз, пока вы не доберетесь до базовых точек, которые json.NET может просто ручка.
this.Name = (string)parsedJson["name"]; this.CreatedTime = DateTime.Parse((string)parsedJson["created_time"]);
и так далее. Эта настройка позволила мне дать json.NET он может обрабатывать настройки без необходимости рефакторинга большой части самой библиотеки или использования громоздких моделей try/parse, которые бы увязли во всей нашей библиотеке из-за количества задействованных объектов. Это также означает, что я могу эффективно обрабатывать любые изменения json на определенном объекте, и мне не нужно беспокоиться обо всем, что касается объекта. Это отнюдь не идеальное решение, но оно работает довольно хорошо от нашего блока и интеграционного тестирования.
несколько лет, и у меня была аналогичная проблема. В моем случае были сильно вложенные интерфейсы и предпочтение для генерации конкретных классов во время выполнения, чтобы он работал с универсальным классом.
Я решил создать прокси-класс во время выполнения, который обертывает объект, возвращенный Newtonsoft.
преимущество этого подхода заключается в том, что он не требует конкретной реализации класса и может обрабатывать любую глубину вложенных интерфейсов автоматически. Вы можете посмотреть больше об этом на моем блог.
using Castle.DynamicProxy; using Newtonsoft.Json.Linq; using System; using System.Reflection; namespace LL.Utilities.Std.Json { public static class JObjectExtension { private static ProxyGenerator _generator = new ProxyGenerator(); public static dynamic toProxy(this JObject targetObject, Type interfaceType) { return _generator.CreateInterfaceProxyWithoutTarget(interfaceType, new JObjectInterceptor(targetObject)); } public static InterfaceType toProxy<InterfaceType>(this JObject targetObject) { return toProxy(targetObject, typeof(InterfaceType)); } } [Serializable] public class JObjectInterceptor : IInterceptor { private JObject _target; public JObjectInterceptor(JObject target) { _target = target; } public void Intercept(IInvocation invocation) { var methodName = invocation.Method.Name; if(invocation.Method.IsSpecialName && methodName.StartsWith("get_")) { var returnType = invocation.Method.ReturnType; methodName = methodName.Substring(4); if (_target == null || _target[methodName] == null) { if (returnType.GetTypeInfo().IsPrimitive || returnType.Equals(typeof(string))) { invocation.ReturnValue = null; return; } } if (returnType.GetTypeInfo().IsPrimitive || returnType.Equals(typeof(string))) { invocation.ReturnValue = _target[methodName].ToObject(returnType); } else { invocation.ReturnValue = ((JObject)_target[methodName]).toProxy(returnType); } } else { throw new NotImplementedException("Only get accessors are implemented in proxy"); } } } }
использование:
var jObj = JObject.Parse(input); InterfaceType proxyObject = jObj.toProxy<InterfaceType>();
Николас Уэстби предоставил отличное решение в потрясающая статья.
если вы хотите десериализовать JSON в один из многих возможных классов, реализующих такой интерфейс:
public class Person { public IProfession Profession { get; set; } } public interface IProfession { string JobTitle { get; } } public class Programming : IProfession { public string JobTitle => "Software Developer"; public string FavoriteLanguage { get; set; } } public class Writing : IProfession { public string JobTitle => "Copywriter"; public string FavoriteWord { get; set; } } public class Samples { public static Person GetProgrammer() { return new Person() { Profession = new Programming() { FavoriteLanguage = "C#" } }; } }
вы можете использовать пользовательский конвертер JSON:
public class ProfessionConverter : JsonConverter { public override bool CanWrite => false; public override bool CanRead => true; public override bool CanConvert(Type objectType) { return objectType == typeof(IProfession); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new InvalidOperationException("Use default serialization."); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var jsonObject = JObject.Load(reader); var profession = default(IProfession); switch (jsonObject["JobTitle"].Value()) { case "Software Developer": profession = new Programming(); break; case "Copywriter": profession = new Writing(); break; } serializer.Populate(jsonObject.CreateReader(), profession); return profession; } }
и вам нужно будет украсить свойство "профессия" атрибутом JsonConverter, чтобы он знал, как использовать ваш пользовательский конвертер:
public class Person { [JsonConverter(typeof(ProfessionConverter))] public IProfession Profession { get; set; } }
и затем, вы можете бросить свой класс с интерфейс:
Person person = JsonConvert.DeserializeObject<Person>(jsonString);
мое решение этого, которое мне нравится, потому что оно красиво общее, выглядит следующим образом:
/// <summary> /// Automagically convert known interfaces to (specific) concrete classes on deserialisation /// </summary> public class WithMocksJsonConverter : JsonConverter { /// <summary> /// The interfaces I know how to instantiate mapped to the classes with which I shall instantiate them, as a Dictionary. /// </summary> private readonly Dictionary<Type,Type> conversions = new Dictionary<Type,Type>() { { typeof(IOne), typeof(MockOne) }, { typeof(ITwo), typeof(MockTwo) }, { typeof(IThree), typeof(MockThree) }, { typeof(IFour), typeof(MockFour) } }; /// <summary> /// Can I convert an object of this type? /// </summary> /// <param name="objectType">The type under consideration</param> /// <returns>True if I can convert the type under consideration, else false.</returns> public override bool CanConvert(Type objectType) { return conversions.Keys.Contains(objectType); } /// <summary> /// Attempt to read an object of the specified type from this reader. /// </summary> /// <param name="reader">The reader from which I read.</param> /// <param name="objectType">The type of object I'm trying to read, anticipated to be one I can convert.</param> /// <param name="existingValue">The existing value of the object being read.</param> /// <param name="serializer">The serializer invoking this request.</param> /// <returns>An object of the type into which I convert the specified objectType.</returns> public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { try { return serializer.Deserialize(reader, this.conversions[objectType]); } catch (Exception) { throw new NotSupportedException(string.Format("Type {0} unexpected.", objectType)); } } /// <summary> /// Not yet implemented. /// </summary> /// <param name="writer">The writer to which I would write.</param> /// <param name="value">The value I am attempting to write.</param> /// <param name="serializer">the serializer invoking this request.</param> public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); } }
}
вы можете очевидно и тривиально преобразовать его в еще более общий конвертер,добавив конструктор, который взял аргумент типа Dictionary
, с помощью которого можно создать экземпляр переменной экземпляра Conversations.
ни один объект не будет когда-нибудьбыть ithingy как интерфейсы все абстрактные по определению.
у вас есть объект, который был сначала сериализован, был какой-то б тип, реализующий аннотация интерфейс. Вы должны иметь это же б класс возродить сериализованные данные.
полученный объект будет иметь некоторый тип, который осуществляет the аннотация интерфейс, который вы ищете.
С документация из этого следует, что вы можете использовать
(Thingy)JsonConvert.DeserializeObject(jsonString, typeof(Thingy));
при десериализации сообщить JSON.NET о конкретном типе.