Кастинг интерфейсов для десериализации в JSON.NET


Я пытаюсь настроить читатель, который будет принимать объекты JSON с различных веб-сайтов (думаю, что информация выскабливается) и переводить их в объекты C#. В настоящее время я использую JSON.NET для процесса десериализации. Проблема, с которой я сталкиваюсь, заключается в том, что он не знает, как обрабатывать свойства уровня интерфейса в классе. Так что-то от природы:

public IThingy Thing

вызывает ошибку:

не удалось создать экземпляр типа IThingy. Тип интерфейс или абстрактный класс не может быть инстанцирован.

относительно важно, чтобы он был IThingy, а не Thingy, поскольку код, над которым я работаю, считается чувствительным, и модульное тестирование очень важно. Издевательство над объектами для атомарных тестовых сценариев невозможно с полноценными объектами, такими как Thingy. Они должны быть интерфейсом.

Я долго изучал JSON.NET ' s документация на некоторое время теперь, и вопросы, которые я мог бы найти на этом сайт, связанный с этим все из более чем год назад. Какая-нибудь помощь?

кроме того, если это имеет значение, мое приложение написано в .NET 4.0.

15 93

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

Json.NET документация

две вещи, которые вы можете попробовать:

реализовать модель 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 о конкретном типе.

в мое решение были добавлены элементы интерфейса в конструкторе.

public class Customer: ICustomer{
     public Customer(Details details){
          Details = details;
     }

     [JsonProperty("Details",NullValueHnadling = NullValueHandling.Ignore)]
     public IDetails Details {get; set;}
}