Использование конвертеров Json.NET для десериализации свойств

88

У меня есть определение класса, которое содержит свойство, возвращающее интерфейс.

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

    public ISomething Thing { get; set; }
}

Попытка сериализовать класс Foo с помощью Json.NET дает мне сообщение об ошибке вроде «Не удалось создать экземпляр типа ISomething. ISomething может быть интерфейсом или абстрактным классом».

Есть ли атрибут или конвертер Json.NET, который позволил бы мне указать конкретный Somethingкласс для использования во время десериализации?

dthrasher
источник
Я считаю, что вам нужно указать имя свойства, которое получает / устанавливает ISomething
ram
У меня есть. Я использую сокращение для автоматически реализуемых свойств, представленное в C # 3.5. msdn.microsoft.com/en-us/library/bb384054.aspx
dthrasher,
4
Разве это не что-то типа. Я думаю, что ram прав, вам все еще нужно имя свойства. Я знаю, что это не связано с вашей проблемой, но ваш комментарий выше заставил меня подумать, что мне не хватает какой-то новой функции в .NET, которая позволяла вам указывать свойство без имени.
Mr Moose

Ответы:

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 06
2
Для Newtonsoft.Json это работает аналогично, но свойство - "$ type"
Jaap
Это было слишком просто!
Шимми Вайцхандлер
1
Остерегайтесь возможных проблем с безопасностью при использовании TypeNameHandling. Дополнительные сведения см. В разделе «Осторожно при обращении с TypeName» в Newtonsoft Json .
dbc
Я вчера как сумасшедший боролся с конвертерами, и это было лучше и понятнее, спасибо !!!
Horothenic
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.

MrMDavidson
источник
Очень хорошо. Придется попробовать конвертер.
dthrasher
4
Будет ли работать тег «[JsonConverter (typeof (TycoonConverter))]», если он есть в списке интерфейса?
Zwik 02
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; }
}
Erhhung
источник
Блестяще. Спасибо :)
Даррен Янг
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;
    }
}

Пока Something реализует ISomething, это должно работать. Также не помещайте пустой конструктор по умолчанию, если конвертер JSon пытается его использовать, вы должны заставить его использовать конструктор, содержащий конкретный тип.

PS. это также позволяет вам сделать ваши сеттеры приватными.

Сэмюэл Дэвис
источник
6
Об этом надо кричать с крыш! Конечно, это добавляет ограничения на конкретную реализацию, но это намного проще, чем другие подходы для тех ситуаций, в которых его можно использовать.
Марк Меуэр,
3
Что, если у нас есть более 1 конструктора с несколькими конкретными типами, он все равно будет знать?
Теоман Шипахи
1
Этот ответ настолько изящен по сравнению со всей запутанной чушью, которую вам пришлось бы делать в противном случае. Это должен быть принятый ответ. Однако в моем случае было одно предостережение: мне пришлось добавить [JsonConstructor] перед конструктором, чтобы он работал ... Я подозреваю, что использование этого только в ОДНОМ из ваших конкретных конструкторов решит вашу (4-летнюю) проблему. @Teomanshipahi
nacitar sevaht
@nacitarsevaht Я могу вернуться и исправить свою проблему сейчас :) в любом случае я даже не помню, что это было, но когда я снова смотрю, это хорошее решение для определенных случаев.
Теоман Шипахи
мы тоже используем это, но я предпочитаю convert в большинстве случаев, потому что связывание конкретного типа с конструктором в первую очередь лишает смысла использование интерфейса для свойства!
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 () вместо заполнения, потому что у моего объекта была более глубокая иерархия. - Использование отражения для получения конструктора и создания его
Аурел
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.
Оливер