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

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

public IThingy Thing

Выдаст ошибку:

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

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

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

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

17 ответов

Решение

@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; }
}

Зачем использовать конвертер? Есть нативная функциональность в Newtonsoft.Json чтобы решить именно эту проблему:

Задавать TypeNameHandling в JsonSerializerSettings в TypeNameHandling.Auto

JsonConvert.SerializeObject(
        toSerialize,
        new JsonSerializerSettings()
        {
          TypeNameHandling = TypeNameHandling.Auto
        });

Это поместит каждый тип в json, который не является конкретным экземпляром типа, а интерфейсом или абстрактным классом.

Я проверил это, и он работает как шарм, даже со списками.

Источник и альтернативная ручная реализация: Code Inside Blog

(Скопировано с этого вопроса)

В тех случаях, когда у меня не было контроля над входящим 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);
    }
}

Используйте этот класс для отображения абстрактного типа в реальный тип:

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

Чтобы включить десериализацию нескольких реализаций интерфейсов, вы можете использовать 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 предлагает то же решение на шаг вперед с помощью универсального JsonConverter.

Николас Уэстби предоставил отличное решение в великолепной статье.

Если вы хотите десериализовать 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);

Я нашел это полезным. Вы тоже можете.

Пример использования

public class Parent
{
    [JsonConverter(typeof(InterfaceConverter<IChildModel, ChildModel>))]
    IChildModel Child { get; set; }
}

Custom Creation Converter

public class InterfaceConverter<TInterface, TConcrete> : CustomCreationConverter<TInterface>
    where TConcrete : TInterface, new()
{
    public override TInterface Create(Type objectType)
    {
        return new TConcrete();
    }
}

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

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

Реализуйте модель проб / разбора:

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

Ни один объект никогда не будет IThingy, так как все интерфейсы являются абстрактными по определению.

У вас есть объект, который был впервые сериализован, имел конкретный тип, реализующий абстрактный интерфейс. Вам нужно, чтобы этот же конкретный класс возродил сериализованные данные.

Полученный объект будет иметь некоторый тип, который реализует абстрактный интерфейс, который вы ищете.

Из документации следует, что вы можете использовать

(Thingy)JsonConvert.DeserializeObject(jsonString, typeof(Thingy));

при десериализации сообщить JSON.NET о конкретном типе.

Предположим, что настройка автофака выглядит следующим образом:

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

Что бы это ни стоило, мне пришлось самому по большей части справиться с этим. Каждый объект имеет метод Deserialize(string jsonStream). Несколько фрагментов этого:

JObject parsedJson = this.ParseJson(jsonStream);
object thingyObjectJson = (object)parsedJson["thing"];
this.Thing = new Thingy(Convert.ToString(thingyObjectJson));

В этом случае new 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 для конкретного объекта, и мне не нужно беспокоиться обо всем, что касается объекта. Это ни в коем случае не идеальное решение, но оно довольно хорошо работает на нашем модульном и интеграционном тестировании.

Несколько лет спустя у меня была похожая проблема. В моем случае были сильно вложенные интерфейсы и предпочтение генерировать конкретные классы во время выполнения, чтобы он работал с универсальным классом.

Я решил создать прокси-класс во время выполнения, который обернет объект, возвращенный 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>();

Мое решение, которое мне нравится, потому что оно довольно общее, заключается в следующем:

/// <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 для создания экземпляра переменной экземпляра преобразования.

Используйте этот JsonKnownTypes, это очень похожий способ использования, он просто добавляет дискриминатор в json:

[JsonConverter(typeof(JsonKnownTypeConverter<Interface1>))]
[JsonKnownType(typeof(MyClass), "myClass")]
public interface Interface1
{  }
public class MyClass : Interface1
{
    public string Something;
}

Теперь при сериализации объекта в json будет добавлено "$type" с участием "myClass" значение, и оно будет использоваться для десериализации

Json:

{"Something":"something", "$type":"derived"}

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

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

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

Вы также можете использовать пользовательский TextInputFormatter, внешние библиотеки не нужны, а также помогает вам понять, как вы можете обрабатывать (де) сериализацию любого типа данных.

      public class MyInputTypeFormatter : TextInputFormatter
{
    public MyInputTypeFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
        SupportedEncodings.Add(Encoding.UTF8);
    }



    protected override bool CanReadType(Type type)
    {
        return type == typeof(MyClass);
    }

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
    {
        var httpContext = context.HttpContext;
        var serviceProvider = httpContext.RequestServices;
        var logger = serviceProvider.GetRequiredService<ILogger<ImageTypeConverter>>();
        using var reader = new StreamReader(httpContext.Request.Body, encoding);
        {
            var data = await reader.ReadToEndAsync();
            if (data.Contains("Hello"))
            {
                var myClass= new MyClass(data);
                return await InputFormatterResult.SuccessAsync(myClass);

            }
            else
            {
                return await InputFormatterResult.FailureAsync();

            }
        }


    }
}

Затем просто добавьте этот модуль форматирования ввода в список форматировщиков ввода с помощью

      services.AddControllers(options=> {
            options.InputFormatters.Insert(0, new MyInputFormatter());
        });

0 здесь означает, что это первый модуль форматирования ввода, вызываемый при привязке модели.

Кажется, что работы много, но по большей части это просто шаблон. Я объясню, как это работает,

У вас есть метод/маршрут действия, который имеет параметр типа MyClass. Когда к нему приходит запрос, вызывается CanReadType вашего средства форматирования ввода, и он возвращает true, что означает, что он будет обрабатывать десериализацию. Затем вызывается метод ReadRequestBodyAsync, и ему передаются данные запроса.

Вы можете делать с данными все, что хотите, и возвращать объект типа MyClass, если ваша десериализация прошла успешно. В противном случае вы просто вернете отказ.

В десериализации вы можете использовать

      using (JsonDocument document = JsonDocument.Parse(jsonString))
{
JsonElement root = document.RootElement;
// ...
}

Вы можете перемещаться по элементам, поскольку входные данные анализируются в объект json, а затем сохраняются в DOM. Затем вы можете увидеть, что они содержат, и вручную создать классы с их данными и преобразовать ваши входные интерфейсы в классы.

Примечание. JsonDocument был представлен в .Net 3.1. Вы можете узнать, как его использовать здесь .

Дополнительные сведения об использовании TextInputFormatter и TextOutputFormatter Преимущество использования пользовательского средства форматирования ввода заключается в том, что он предоставляет центральный класс для обработки ваших пользовательских классов, которые могут использовать несколько интерфейсов. Это также дает вам точный контроль над обработкой входных данных.

Другие вопросы по тегам