Використання перетворювачів Json.NET для десериалізації властивостей


88

У мене є визначення класу, яке містить властивість, що повертає інтерфейс.

public class Foo
{ 
    public int Number { get; set; }

    public ISomething Thing { get; set; }
}

Спроба серіалізації класу Foo за допомогою Json.NET видає мені повідомлення про помилку на кшталт: "Не вдалося створити екземпляр типу" ISomething ". ISomething може бути інтерфейсом або абстрактним класом."

Чи існує атрибут Json.NET або конвертер, який дозволив би мені вказати конкретний Somethingклас для використання під час десеріалізації?


Я вважаю, що вам потрібно вказати назву властивості, яке отримує / встановлює ISomething
ram

У мене є. Я використовую скорочення для автоматично реалізованих властивостей, представлене в C # 3.5. msdn.microsoft.com/en-us/library/bb384054.aspx
dthrasher

4
Чи не ISomething типу. Я думаю, що ram має рацію, вам все одно потрібна назва власності. Я знаю, що це не пов’язано з вашою проблемою, але ваш коментар вище змусив мене думати, що мені не вистачає якоїсь нової функції в .NET, яка дозволила вам вказати властивість без імені.
Містер Муз,

Відповіді:


92

Одне з речей, яке ви можете зробити з Json.NET :

var settings = new JsonSerializerSettings();
settings.TypeNameHandling = TypeNameHandling.Objects;

JsonConvert.SerializeObject(entity, Formatting.Indented, settings);

TypeNameHandlingпрапор додасть$type властивість в форматі JSON, який дозволяє Json.NET знати , який конкретний тип він повинен десеріалізациі об'єкта в. Це дозволяє вам десериалізувати об'єкт, одночасно виконуючи інтерфейс або абстрактний базовий клас.

Однак недоліком є ​​те, що це дуже специфічно для Json.NET. $type буде повністю кваліфікований тип, тому, якщо ви серіалізуєте його за допомогою інформації про тип, десериалізатор також повинен його зрозуміти.

Документація: Налаштування серіалізації за допомогою Json.NET


Цікаво. Мені доведеться пограти з цим. Приємна порада!
dthrasher

2
Для Newtonsoft.Json це працює аналогічно, але властивість "$ type"
Яап,

Це було занадто просто!
Шіммі Вайцхандлер,

1
Тут слідкуйте за можливими проблемами безпеки TypeNameHandling. Детальніше див. У розділі TypeNameHandling Handling у Newtonsoft Json .
dbc

Я вчора боровся як божевільний з конвертерами, і це було набагато краще і зрозуміліше, дякую !!!
Горотеніка

52

Ви можете досягти цього за допомогою класу JsonConverter. Припустимо, у вас є клас із властивістю інтерфейсу;

public class Organisation {
  public string Name { get; set; }

  [JsonConverter(typeof(TycoonConverter))]
  public IPerson Owner { get; set; }
}

public interface IPerson {
  string Name { get; set; }
}

public class Tycoon : IPerson {
  public string Name { get; set; }
}

Ваш JsonConverter відповідає за серіалізацію та десеріалізацію базового майна;

public class TycoonConverter : JsonConverter
{
  public override bool CanConvert(Type objectType)
  {
    return (objectType == typeof(IPerson));
  }

  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  {
    return serializer.Deserialize<Tycoon>(reader);
  }

  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  {
    // Left as an exercise to the reader :)
    throw new NotImplementedException();
  }
}

Коли ви працюєте з організацією, десеріалізованою за допомогою Json.Net, базовий IPerson для властивості Owner матиме тип Tycoon.


Дуже хороша. Мені доведеться спробувати конвертер.
dthrasher

4
Чи міг "[JsonConverter (typeof (TycoonConverter))]]" все одно працюватиме, якби він був у списку інтерфейсу?
Zwik

40

Замість передачі настроюваного об'єкта JsonSerializerSettings JsonConvert.SerializeObject () з параметром TypeNameHandling.Objects, як уже згадувалося, ви можете просто позначити це властивість інтерфейсу атрибутом, щоб згенерований JSON не роздувався властивостями "$ type" на КОЖНОМУ об’єкті:

public class Foo
{
    public int Number { get; set; }

    // Add "$type" property containing type info of concrete class.
    [JsonProperty( TypeNameHandling = TypeNameHandling.Objects )]
    public ISomething { get; set; }
}

Блискуче. Дякую :)
Даррен Янг

5
Для колекцій інтерфейсів або абстрактних класів властивістю є "ItemTypeNameHandling". наприклад: [JsonProperty (ItemTypeNameHandling = TypeNameHandling.Auto)]
Ентоні Ф

Дякую за це!
brudert

23

В найновішій версії незалежного перетворювача Newtonsoft Json ви можете встановити конструктор із конкретним типом, що стосується властивості взаємодії.

public class Foo
{ 
    public int Number { get; private set; }

    public ISomething IsSomething { get; private set; }

    public Foo(int number, Something concreteType)
    {
        Number = number;
        IsSomething = concreteType;
    }
}

Поки щось реалізує ISomething, це повинно працювати. Також не ставте порожній конструктор за замовчуванням, якщо перетворювач JSon намагається це використовувати, ви повинні змусити його використовувати конструктор, що містить конкретний тип.

PS. це також дозволяє зробити ваших установників приватними.


6
Це треба кричати з дахів! Правда, це додає обмеження щодо конкретної реалізації, але набагато простіше, ніж інші підходи для тих ситуацій, коли це може бути використано.
Mark Meuer

3
Що робити, якщо у нас є більше 1 конструктора з декількома типами бетону, чи все одно він знатиме?
Teoman shipahi

1
Ця відповідь настільки елегантна в порівнянні з усіма заплутаними нісенітницями, які вам доводилося б робити інакше. Це має бути прийнятою відповіддю. Однак одним застереженням у моєму випадку було те, що мені довелося додати [JsonConstructor] перед конструктором, щоб він працював .... Я підозрюю, що використання цього лише на ОДНОМУ з ваших бетонних конструкторів вирішить вашу (4-річну) проблему @Teomanshipahi
nacitar sevaht

@nacitarsevaht Я можу повернутися назад і вирішити свою проблему зараз :) в будь-якому випадку, я навіть не пам'ятаю, що це було, але коли я переглядаю це хороше рішення для певних випадків.
Teoman shipahi

ми використовуємо це теж, але я в більшості випадків віддаю перевагу перетворенню, тому що сполучення конкретного типу з конструктором перешкоджає цілі використання інтерфейсу для властивості!
gabe,

19

У мене була та ж проблема, тому я придумав власний конвертер, який використовує аргумент відомих типів.

public class JsonKnownTypeConverter : JsonConverter
{
    public IEnumerable<Type> KnownTypes { get; set; }

    public JsonKnownTypeConverter(IEnumerable<Type> knownTypes)
    {
        KnownTypes = knownTypes;
    }

    protected object Create(Type objectType, JObject jObject)
    {
        if (jObject["$type"] != null)
        {
            string typeName = jObject["$type"].ToString();
            return Activator.CreateInstance(KnownTypes.First(x =>typeName.Contains("."+x.Name+",")));
        }

        throw new InvalidOperationException("No supported type");
    }

    public override bool CanConvert(Type objectType)
    {
        if (KnownTypes == null)
            return false;

        return (objectType.IsInterface || objectType.IsAbstract) && KnownTypes.Any(objectType.IsAssignableFrom);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // Load JObject from stream
        JObject jObject = JObject.Load(reader);
        // Create target object based on JObject
        var target = Create(objectType, jObject);
        // Populate the object properties
        serializer.Populate(jObject.CreateReader(), target);
        return target;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Я визначив два методи розширення для десериалізації та серіалізації:

public static class AltiJsonSerializer
{
    public static T DeserializeJson<T>(this string jsonString, IEnumerable<Type> knownTypes = null)
    {
        if (string.IsNullOrEmpty(jsonString))
            return default(T);

        return JsonConvert.DeserializeObject<T>(jsonString,
                new JsonSerializerSettings
                {
                    TypeNameHandling = TypeNameHandling.Auto, 
                    Converters = new List<JsonConverter>
                        (
                            new JsonConverter[]
                            {
                                new JsonKnownTypeConverter(knownTypes)
                            }
                        )
                }
            );
    }

    public static string SerializeJson(this object objectToSerialize)
    {
        return JsonConvert.SerializeObject(objectToSerialize, Formatting.Indented,
        new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.Auto});
    }
}

Ви можете визначити свій власний спосіб порівняння та ідентифікації типів у конвертерах, я використовую лише ім'я класу.


1
Цей JsonConverter чудовий, я використовував його, але зіткнувся з кількома проблемами, які я вирішив таким чином: - Використовуючи JsonSerializer.CreateDefault () замість Заповнення, оскільки мій об’єкт мав глибшу ієрархію. - Використання рефлексії для отримання конструктора та інстанціації його за допомогою методу Create ()
Aurel

3

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

public class Model
{
    [JsonConverter(typeof(ConcreteTypeConverter<Something>))]
    public ISomething TheThing { get; set; }
}

Тут просто використовується реалізація серіалізатора за замовчуванням від Json.Net, одночасно вказуючи конкретний тип.

Вихідний код та огляд доступні в цьому дописі в блозі .


1
Це чудове рішення. Ура.
JohnMetta

2

Я просто хотів закінчити приклад, який @Daniel T. показав нам вище:

Якщо ви використовуєте цей код для серіалізації об’єкта:

var settings = new JsonSerializerSettings();
settings.TypeNameHandling = TypeNameHandling.Objects;
JsonConvert.SerializeObject(entity, Formatting.Indented, settings);

Код для десеріалізації json повинен виглядати так:

var settings = new JsonSerializerSettings(); 
settings.TypeNameHandling = TypeNameHandling.Objects;
var entity = JsonConvert.DeserializeObject<EntityType>(json, settings);

Ось як json пристосовується при використанні TypeNameHandlingпрапора:введіть тут опис зображення


-5

Я дивувався цьому самому, але боюся, цього не можна зробити.

Давайте подивимось на це так. Ви передаєте JSon.net рядок даних і тип, для десериалізації. Що робити JSON.net, коли потрапляє на щось ISomething? Він не може створити новий тип ISomething, оскільки ISomething не є об’єктом. Він також не може створити об'єкт, який реалізує ISomething, оскільки він не знає, який із багатьох об'єктів, які можуть успадкувати ISomething, йому слід використовувати. Інтерфейси - це те, що можна автоматично серіалізувати, але не автоматично десеріалізувати.

Що б я зробив, це було б поглянути на заміну ISomething базовим класом. Використовуючи це, ви могли б отримати ефект, який шукаєте.


1
Я розумію, що це не спрацює "нестандартно". Але мені було цікаво, чи є якийсь атрибут на зразок "[JsonProperty (typeof (SomethingBase))]" ", який я міг би використати для забезпечення конкретного класу.
dthrasher

То чому б не використовувати SomethingBase замість ISomething у наведеному вище коді? Можна стверджувати, що ми також дивимось на це неправильно, оскільки інтерфейси не повинні використовуватися в серіалізації, оскільки вони просто визначають "інтерфейс" зв'язку з даним класом. Технічно серіалізувати інтерфейс - це нісенітниця, як і серіалізувати абстрактний клас. Тож хоча це "можна зробити", я стверджую, що "цього не слід робити".
Тімоті Болдрідж

Ви розглядали будь-який із класів у просторі імен Newtonsoft.Json.Serialization? зокрема клас JsonObjectContract?
Джонні,

-9

Ось посилання на статтю, написану ScottGu

На основі цього я написав певний код, який, на мою думку, може бути корисним

public interface IEducationalInstitute
{
    string Name
    {
        get; set;
    }

}

public class School : IEducationalInstitute
{
    private string name;
    #region IEducationalInstitute Members

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    #endregion
}

public class Student 
{
    public IEducationalInstitute LocalSchool { get; set; }

    public int ID { get; set; }
}

public static class JSONHelper
{
    public static string ToJSON(this object obj)
    {
        JavaScriptSerializer serializer = new JavaScriptSerializer();
        return serializer.Serialize(obj);
    }
    public  static string ToJSON(this object obj, int depth)
    {
        JavaScriptSerializer serializer = new JavaScriptSerializer();
        serializer.RecursionLimit = depth;
        return serializer.Serialize(obj);
    }
}

І ось як би ви це назвали

School myFavSchool = new School() { Name = "JFK High School" };
Student sam = new Student()
{
    ID = 1,
    LocalSchool = myFavSchool
};
string jSONstring = sam.ToJSON();

Console.WriteLine(jSONstring);
//Result {"LocalSchool":{"Name":"JFK High School"},"ID":1}

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


1
Ваш зразок використовує JavaScriptSerializer, клас у .NET Framework. Я використовую Json.NET як свій серіалізатор. codeplex.com/Json
dthrasher

3
Не посилається на вихідне запитання, там чітко згадано Json.NET.
Олівер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.