Интерфейсы для десериализации в JSON.NET
Я пытаюсь настроить читателя, который будет принимать объекты JSON с разных сайтов (продумывать информацию) и переводить их на объекты С#. В настоящее время я использую JSON.NET для процесса десериализации. Проблема, с которой я сталкиваюсь, заключается в том, что она не знает, как обрабатывать свойства уровня интерфейса в классе. Так что-то от природы:
public IThingy Thing
Выдает ошибку:
Не удалось создать экземпляр типа IThingy. Тип - это интерфейс или абстрактный класс и не может быть создан.
Относительно важно, чтобы это был IThingy, а не Thingy, поскольку код, над которым я работаю, считается чувствительным, а модульное тестирование очень важно. Откусывание объектов для атомных тестовых скриптов невозможно с помощью полнофункциональных объектов, таких как Thingy. Они должны быть интерфейсом.
Я уже давно разбираюсь в документации JSON.NET, и вопросы, которые я мог найти на этом сайте, связаны с этим более года назад. Любая помощь?
Кроме того, если это имеет значение, мое приложение написано в .NET 4.0.
Ответы
Ответ 1
@SamualDavis обеспечил отличное решение в связанном вопросе, который я опишу здесь.
Если вам нужно десериализовать поток JSON в конкретный класс с свойствами интерфейса, вы можете включить конкретные классы в качестве параметров для конструктора для класса! Deserializer 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; }
}
Ответ 2
Чтобы включить десериализацию нескольких реализаций интерфейсов, вы можете использовать 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 не нужно вставлять имена конкретных типов.
Этот SO post предлагает одно и то же решение на один шаг с общим JsonConverter.
Ответ 3
(Скопировано из этот вопрос)
В случаях, когда я не контролировал входящий 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);
}
}
Ответ 4
Зачем использовать конвертер? В Newtonsoft.Json
есть встроенная функциональность для решения этой конкретной проблемы:
Установите TypeNameHandling
в JsonSerializerSettings
на TypeNameHandling.Auto
JsonConvert.SerializeObject(
toSerialize,
new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.Auto
});
Это поместит каждый тип в json, который не хранится как конкретный экземпляр типа, а как интерфейс или абстрактный класс.
Я тестировал его, и он работает как шарм, даже со списками.
Источник и альтернативная ручная реализация: Код внутри блога
Ответ 5
Я нашел это полезным. Вы тоже можете.
Пример использования
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();
}
}
Документация Json.NET
Ответ 6
Две вещи, которые вы можете попробовать:
Внедрить модель 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 и вашими листовыми объектами и десериализуйте его.
Первый может потенциально не работать во время выполнения, второй требует изменений в вашей объектной модели и гомогенизирует вывод до самого низкого общего знаменателя.
Ответ 7
Для тех, кому может быть интересно, о 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);
}
}
Ответ 8
Для чего это стоило, мне в итоге пришлось самому справиться с этим сам по большей части. Каждый объект имеет метод Deserialize (string jsonStream). Несколько его фрагментов:
JObject parsedJson = this.ParseJson(jsonStream);
object thingyObjectJson = (object)parsedJson["thing"];
this.Thing = new Thingy(Convert.ToString(thingyObjectJson));
В этом случае новый Thingy (string) является конструктором, который будет вызывать метод Deserialize (string jsonStream) соответствующего конкретного типа. Эта схема будет продолжать идти вниз и вниз, пока вы не перейдете к базовым точкам, которые может обрабатывать только json.NET.
this.Name = (string)parsedJson["name"];
this.CreatedTime = DateTime.Parse((string)parsedJson["created_time"]);
Итак, и т.д. Эта настройка позволила мне дать настройки json.NET, с которыми она может справиться, без необходимости реорганизовывать большую часть самой библиотеки или использовать громоздкие модели try/parse, которые увязли бы всю нашу библиотеку из-за количества задействованных объектов. Это также означает, что я могу эффективно обрабатывать любые изменения json на конкретном объекте, и мне не нужно беспокоиться обо всем, что касается объекта. Это отнюдь не идеальное решение, но оно отлично работает с нашим модулем и тестированием интеграции.
Ответ 9
Предположим, что параметр 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
Ответ 10
Мое решение для этого, которое мне нравится, потому что оно красиво общее, выглядит следующим образом:
/// <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 < Type, Type > с помощью которого создается экземпляр экземпляра конверсий.
Ответ 11
Несколько лет спустя, и у меня была аналогичная проблема. В моем случае были сильно вложенные интерфейсы и предпочтение генерации конкретных классов во время выполнения, чтобы он работал с общим классом.
Я решил создать прокси-класс во время выполнения, который обертывает объект, возвращенный 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>();
Ответ 12
Ни один объект не будет когда-либо быть IThingy, поскольку интерфейсы являются абстрактными по определению.
Объект, который у вас есть, который был сначала сериализован, был определенного типа, реализующего абстрактный интерфейс. Вы должны иметь этот же конкретный класс, чтобы оживить сериализованные данные.
Получаемый объект будет тогда определенного типа, который реализует абстрактный интерфейс, который вы ищете.
Из документации следует, что вы можете использовать
(Thingy)JsonConvert.DeserializeObject(jsonString, typeof(Thingy));
при десериализации для информирования JSON.NET о конкретном типе.
Ответ 13
В мое решение были добавлены элементы интерфейса в конструкторе.
public class Customer: ICustomer{
public Customer(Details details){
Details = details;
}
[JsonProperty("Details",NullValueHnadling = NullValueHandling.Ignore)]
public IDetails Details {get; set;}
}