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


128

Я намагаюся налаштувати зчитувач, який буде приймати в JSON об’єкти з різних веб-сайтів (думаю, скрабовування інформації) та переводив їх у об'єкти C #. Зараз я використовую JSON.NET для процесу десеріалізації. Проблема, з якою я стикаюся, полягає в тому, що він не знає, як обробляти властивості рівня інтерфейсу в класі. Отже, щось із природи:

public IThingy Thing

Виведе помилку:

Не вдалося створити екземпляр типу IThingy. Тип - це інтерфейс або абстрактний клас, і його неможливо створити.

Надзвичайно важливо, щоб це був IThingy на відміну від Thingy, оскільки код, над яким я працюю, вважається чутливим, а тестування модулів є надзвичайно важливим. Знущання з об’єктів для атомних тестових скриптів неможливо з повноцінними об'єктами типу Thingy. Вони повинні бути інтерфейсом.

Я деякий час переглядав документацію JSON.NET, і питання, які я міг знайти на цьому веб-сайті, пов’язані з цим, виникають понад рік тому. Будь-яка допомога?

Крім того, якщо це має значення, моя програма написана на .NET 4.0.


Відповіді:


115

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

15
Як би це працювало з ICollection? ICollection <IGuest> Гості {get; set;}
DrSammyD

12
Він працює з ICollection <ConcreteClass>, тому ICollection <Guest> працює. Як FYI, ви можете помістити атрибут [JsonConstructor] на свій конструктор, щоб він використовував це за замовчуванням, якщо у вас є кілька конструкторів
DrSammyD

6
Я застряг в одній проблемі, в моєму випадку у мене є кілька реалізацій інтерфейсу (у вашому прикладі інтерфейс ILocation), що робити, якщо є такі класи, як MyLocation, VIPLocation, OrdinaryLocation. Як відобразити їх у власність Location? Якщо у вас просто є така реалізація, як MyLocation, її легко, але як це зробити, якщо існує кілька реалізацій ILocation?
АТЕР

10
Якщо у вас є більше одного конструктора, ви можете позначити спеціальний конструктор [JsonConstructor]атрибутом.
Доктор Роб Ленг

26
Це зовсім не добре. Сенс використання інтерфейсів полягає у використанні введення залежності, але, роблячи це за допомогою параметра, введеного об'єктом, необхідного вашим конструктором, ви повністю викручуєте точку існування інтерфейсу як властивості.
Jérôme MEVEL

57

(Скопійовано з цього питання )

У тих випадках, коли я не мав контролю над вхідним JSON (і тому не можу забезпечити, щоб він включав властивість типу $), я написав спеціальний конвертер, який дозволяє чітко вказати конкретний тип:

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

11
Мені дуже подобається такий підхід і застосовано його до нашого власного проекту. Я навіть додав a ConcreteListTypeConverter<TInterface, TImplementation>для обробки членів класу типу IList<TInterface>.
Олівер

3
Це чудовий біт коду. Хоча було б приємніше мати фактичний код concreteTypeConverterу питанні.
Кріс

2
@Oliver - Чи можете ви розмістити свою ConcreteListTypeConverter<TInterface, TImplementation>реалізацію?
Майкл

2
А якщо у вас є два реалізатори ISomething?
bdaniel7

56

Навіщо використовувати перетворювач? Існує нативна функціональність Newtonsoft.Jsonдля вирішення цієї точної проблеми:

Набір TypeNameHandlingв JsonSerializerSettingsдоTypeNameHandling.Auto

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

Це додасть кожен тип до json, який не має бути конкретним екземпляром типу, а інтерфейсом або абстрактним класом.

Переконайтеся, що ви використовуєте однакові налаштування для серіалізації та десеріалізації .

Я перевірив це, і він працює як шарм, навіть зі списками.

Веб-результат пошуку результатів із посиланнями на сайт

⚠️ ПОПЕРЕДЖЕННЯ :

Використовуйте це лише для json з відомого та надійного джерела. Користувач snipsnipsnip правильно зазначив, що це справді вразливість.

Див. CA2328 та SCS0028 для отримання додаткової інформації.


Джерело та альтернативна ручна реалізація: Code Inside Blog


3
Відмінно, це допомогло мені для швидкого і брудного глибокого клону ( stackoverflow.com/questions/78536/deep-cloning-objects )
Compufreak

1
@Shimmy Objects: "Включіть ім'я типу .NET при серіалізації в структуру об'єктів JSON." Авто: Включіть ім'я типу .NET, коли тип об'єкта, що серіалізується, не такий, як його оголошений тип. Зауважте, що це за замовчуванням не включає кореневий серіалізований об'єкт. Щоб включити ім'я типу кореневого об'єкта в форматі JSON, необхідно вказати об'єкт кореневого типу з SerializeObject (Object, Type, JsonSerializerSettings) або Serialize (JsonWriter, Object, Type) » . Джерело: newtonsoft.com/json/help/html / ...
Мафія

4
Я просто спробував це на Deserialization, і це не працює. Тема цього питання щодо переповнення стека - "Кастинг інтерфейсів для десеріалізації в JSON.NET"
Джастін Руссо

3
@JustinRusso працює лише тоді, коли json був серіалізований з тією ж настройкою
Mafii

3
Підкажіть за швидке, якщо не брудне рішення. Якщо ви просто серіалізуєте конфігурації, це працює. Удари, що зупиняють розробку для створення перетворювачів, і, безумовно, перемагає прикрашати кожне введене майно. serializer.TypeNameHandling = TypeNameHandling.Auto; JsonConvert.DefaultSettings (). TypeNameHandling = TypeNameHandling.Auto;
Шон Андерсон

39

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


Чи не буде виклик методу WriteJson на serializer.Serialize викликати переповнення стека, оскільки виклик serialize за значенням, що серіалізується перетворювачем, спричинить повторний виклик методу WriteJson перетворювача?
Трайнко

Це не повинно, якщо метод CanConvert () повертає послідовний результат.
Ерік Буменділ

3
Чому ви порівнюєте FullNames, коли ви можете просто порівнювати типи безпосередньо?
Олексій Жуковський

Просто порівняння типів також чудово.
Ерік Буменділ

23

Використовуйте цей клас для відображення абстрактного типу в реальний тип:

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

1
Мені дуже подобається приємна стисла відповідь, яка вирішує мою проблему. Ніякої необхідності в автофакелі чи нічого!
Бен Пауер

3
Варто поставити це до декларації про клас перетворювача: where TReal : TAbstractщоб переконатися, що він може бути
переданий

1
Повніше, де могло бути where TReal : class, TAbstract, new().
Ерік Філіпс

2
Я також використовував цей конвертер з stru. Я вважаю, що "достатньо TReal: TAb Abstract". Дякую усім.
Gildor

2
Золото! Гладкий шлях.
SwissCoder

12

Ніколас Вестбі запропонував чудове рішення у приголомшливій статті .

Якщо ви хочете десеріалізувати 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;
    }
}

І вам потрібно буде прикрасити властивість "Profession" атрибутом JsonConverter, щоб він міг використовувати ваш спеціальний конвертер:

    public class Person
    {
        [JsonConverter(typeof(ProfessionConverter))]
        public IProfession Profession { get; set; }
    }

А потім ви можете передавати свій клас за допомогою інтерфейсу:

Person person = JsonConvert.DeserializeObject<Person>(jsonString);

8

Ви можете спробувати дві речі:

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

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 та вашими листовими об'єктами та деріаріалізуйте його.

Перший може потенційно вийти з ладу під час виконання, другий вимагає змін у вашій об'єктній моделі та гомогенізує вихід до найменшого загального знаменника.


Модель спробувати / розбору неможлива через масштаб, з яким я маю працювати. Я повинен розглянути сферу сотень базових об'єктів з ще більшою кількістю об'єктів заглушки / помічника, щоб представити вбудовані JSON-об'єкти, які трапляються багато. Про те, щоб змінити модель об'єкта, не виникає сумніву, але чи не використання конкретного базового класу у властивостях не дає нам змоги знущатися над елементами для тестування одиниць? Або я якось відступаю?
tmesser

Ви все ще можете реалізувати макет із IPerson - зауважте, що типом властивості Organisation.Owner все ще є IPerson. Але для десеріалізації довільної цілі вам потрібно повернути конкретний тип. Якщо ви не володієте визначенням типу і не можете визначити мінімальний набір властивостей, який знадобиться вашому коду, то останньою інстанцією є щось на зразок пакета ключ / значення. Використовуючи коментар зі свого прикладу у Facebook - чи можете ви опублікувати відповідь, як виглядають ваші (одна чи кілька) реалізацій ILocation? Це може допомогти рухати речі вперед.
mcw

Оскільки основна надія знущається, інтерфейс ILocation насправді є лише фасадом для конкретного об'єкта Location. Швидкий приклад, який я щойно опрацював, - це щось подібне ( pastebin.com/mWQtqGnB ) для інтерфейсу, і це ( pastebin.com/TdJ6cqWV ) для конкретного об'єкта.
tmesser

І щоб перейти до наступного кроку, це приклад того, як виглядатиме IPage ( pastebin.com/iuGifQXp ) та Page ( pastebin.com/ebqLxzvm ). Проблема, звичайно, полягає в тому, що, хоча дезаріалізація сторінки, як правило, спрацює нормально, вона задихнеться, коли потрапить до властивості ILocation.
tmesser

Гаразд, так що розмірковуючи про об’єкти, які ви насправді викреслюєте та десеріалізуєте - чи взагалі так трапляється, що дані JSON узгоджуються з конкретним визначенням класу? Це означає (гіпотетично), що ви не зіткнетесь з "локаціями" з додатковими властивостями, які зробили б розташування непридатним для використання в якості конкретного типу для дезаріалізованого об'єкта? Якщо так, присвоєння властивості ILocation Page за допомогою "LocationConverter" має працювати. Якщо ні, то це тому, що дані JSON не завжди відповідають жорсткій або стійкій структурі (наприклад, ILocation), тоді (... продовження)
mcw

8

Я вважав це корисним. Ви також можете.

Приклад використання

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


1
Не працездатне рішення. Не стосується списків і призводить до розповсюдження декораторів / анотацій скрізь.
Шон Андерсон

5

Для тих, хто може бути цікавим про 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);
    }
}

1
Я плутаю з переоціненим CanConvert(Type objectType) { return true;}. Здається, хакі, як саме це корисно? Я можу помилятися, але хіба це не так, як сказати меншому недосвідченому бійцю, що вони виграють бій незалежно від супротивника?
Chef_Code

4

Чому це варто, я зрештою змушений з цим боротися здебільшого сам. Кожен об’єкт має метод Deserialize (string jsonStream) . Кілька фрагментів:

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

У цьому випадку новий Thingy (string) - це конструктор, який викликатиме метод Deserialize (рядок jsonStream) відповідного конкретного типу. Ця схема буде йти вниз і вниз, поки ви не дістанетесь до базових точок, які json.NET може просто обробити.

this.Name = (string)parsedJson["name"];
this.CreatedTime = DateTime.Parse((string)parsedJson["created_time"]);

І так далі. Ця настройка дозволила мені надати установки json.NET, з якими вона може працювати без необхідності переробляти велику частину самої бібліотеки або використовувати незграбні спроби / розбору моделей, які б завалили всю нашу бібліотеку через кількість об'єктів, що беруть участь. Це також означає, що я можу ефективно обробляти будь-які зміни json на конкретному об’єкті, і мені не потрібно турбуватися про все, що стосується об'єкта. Це аж ніяк не ідеальне рішення, але воно працює досить добре з нашого модуля та інтеграційного тестування.


4

Припустимо, налаштування автоматичного факсу на зразок наступного:

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


Я буду голосувати за це як найкраще рішення. DI настільки широко використовуються в наші дні c # web devs, і це чудово підходить як централізоване місце для обробки цих типів перетворення роздільною здатністю.
appletwo

3

Жоден об'єкт ніколи не буде IThingy, оскільки інтерфейси всі абстрактні за визначенням.

Об'єкт ви , що був першим серіалізовать був якийсь - то конкретного типу, реалізації абстрактного інтерфейсу. Потрібно, щоб цей самий конкретний клас відроджував серіалізовані дані.

Отриманий об'єкт буде мати якийсь - то тип , який реалізує абстрактний інтерфейс , якого ви шукаєте.

З документації випливає, що ви можете використовувати

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

при десаріалізації повідомляти JSON.NET про тип бетону.


Саме на цю посаду я звертався більше року тому. Єдина основна пропозиція (написання користувальницьких перетворювачів) не є надзвичайно можливою з масштабом, який я змушений вважати. JSON.NET сильно змінився протягом року. Я чудово розумію різницю між класом та інтерфейсом, але C # також підтримує неявні перетворення від інтерфейсу до об'єкта, який реалізує інтерфейс щодо типізації. Я по суті запитую, чи є спосіб сказати JSON.NET, який об’єкт реалізує цей інтерфейс.
tmesser

Це було все у відповіді, на яку я вказував вам. Переконайтеся, що є _typeвластивість, яка сигналізує конкретний тип про використання.
Шон Кінсі

І я сильно сумніваюся, що C # підтримує будь-який тип "неявного" набору тексту від змінної, оголошеної як інтерфейс до конкретного типу, без будь-яких натяків.
Шон Кінсі

Якщо я не прочитав його неправильно, властивість _type повинно було бути в JSON для серіалізації. Це чудово працює, якщо ви лише десеріалізуєте те, що ви вже серіалізували, але це не те, що відбувається тут. Я витягую JSON з багатьох сайтів, які не дотримуватимуться цього стандарту.
tmesser

@YYY - Чи контролюєте ви серіалізацію та десеріалізацію з джерела JSON? Тому що в кінцевому підсумку вам потрібно буде або вбудувати тип бетону в серіалізований JSON як підказку для використання при десеріалізації, або вам доведеться використовувати якусь модель спробу / розбору, яка виявляє / намагається виявити тип бетону під час виконання і викликати відповідний десеріалізатор.
mcw

3

Моє рішення цього рішення, яке мені подобається, оскільки воно загальноприйняте, таке:

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

}

Можна, очевидно, і тривіально перетворити його в ще більш загальний конвертер, додавши конструктор, який взяв аргумент типу словник <Тип, Тип>, за допомогою якого інстанціювати змінну екземпляра конверсій.


3

Кілька років і у мене було подібне питання. У моєму випадку були сильно вкладені інтерфейси та перевага для генерації конкретних класів під час виконання, щоб це працювало з загальним класом.

Я вирішив створити проксі-клас під час виконання, який обгортає об'єкт, повернутий 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.
Філіп Піттл

Нема проблем. Я трохи здивувався, побачивши, що там нічого немає. З цього оригінального прикладу він трохи змінився, тому я вирішив поділитися кодом. github.com/sudsy/JsonDuckTyper . Я також опублікував його на nuget як JsonDuckTyper. Якщо ви виявите, що хочете вдосконалити його, просто надішліть мені PR, і я з радістю покладу на вас зобов'язання.
Суді

Коли я шукав рішення у цій галузі, я також натрапив на github.com/ekonbenefits/impromptu-interface . У моєму випадку воно не працює, оскільки не підтримує dotnet core 1.0, але може працювати для вас.
Суді

Я намагався з Impromptu Interface, але Json.Net не був радий робити PopulateObjectпроксі, генерований Impromptu Interface. Я, на жаль, відмовився від Duck Typing - було просто простіше створити спеціальний Json Contract Serializer, який використовував відображення для пошуку існуючої реалізації потрібного інтерфейсу та використання цього.
Філіп Піттл

1

Використовуйте цей JsonK knownTypes , це дуже подібний спосіб використання, він просто додає дискримінатор до 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"}

0

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

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

     [JsonProperty("Details",NullValueHnadling = NullValueHandling.Ignore)]
     public IDetails Details {get; set;}
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.