Приведение интерфейсов для десериализации в JSON.NET - PullRequest
111 голосов
/ 25 апреля 2011

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

public IThingy Thing

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

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

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

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

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

Ответы [ 15 ]

97 голосов
/ 09 августа 2013

@ 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; }
}
48 голосов
/ 30 августа 2012

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

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

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

Установите TypeNameHandling в JsonSerializerSettings на TypeNameHandling.Auto

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

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

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

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

36 голосов
/ 07 октября 2012

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

10 голосов
/ 22 февраля 2018

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

public class AbstractConverter<TReal, TAbstract> : JsonConverter where TReal : TAbstract
{
    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);
6 голосов
/ 21 марта 2016

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

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

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

5 голосов
/ 25 апреля 2011

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

Реализовать модель 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и ваш лист объектов, и десериализовать к нему.

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

4 голосов
/ 24 февраля 2018

Николас Уэстби (Nicholas Westby) предоставил отличное решение в потрясающей статье .

Если вы хотите десериализовать 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);
4 голосов
/ 10 июня 2015

Для тех, кому может быть интересно узнать о 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);
    }
}
3 голосов
/ 05 декабря 2016

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...