Как реализовать пользовательский JsonConverter в JSON.NET для десериализации списка объектов базового classа?

Я пытаюсь расширить пример JSON.net, приведенный здесь http://james.newtonking.com/projects/json/help/CustomCreationConverter.html

У меня есть другой подclass, полученный из базового classа / интерфейса

public class Person { public string FirstName { get; set; } public string LastName { get; set; } } public class Employee : Person { public string Department { get; set; } public string JobTitle { get; set; } } public class Artist : Person { public string Skill { get; set; } } List people = new List { new Employee(), new Employee(), new Artist(), }; 

Как я десериализую следующий Json обратно в List

 [ { "Department": "Department1", "JobTitle": "JobTitle1", "FirstName": "FirstName1", "LastName": "LastName1" }, { "Department": "Department2", "JobTitle": "JobTitle2", "FirstName": "FirstName2", "LastName": "LastName2" }, { "Skill": "Painter", "FirstName": "FirstName3", "LastName": "LastName3" } ] 

Я не хочу использовать TypeNameHandling JsonSerializerSettings. Я специально ищу пользовательскую реализацию JsonConverter, чтобы справиться с этим. Документация и примеры вокруг этого довольно скудны в сети. Кажется, я не могу получить переопределенную реализацию метода ReadJson () в JsonConverter.

Используя стандартный CustomCreationConverter , я изо всех сил пытался работать, как генерировать правильный тип ( Person или Employee ), потому что для его определения вам нужно проанализировать JSON, и нет встроенного способа сделать это с помощью метода Create .

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

Требуется подclass JsonConverter , переопределяющий метод ReadJson и создающий новый абстрактный метод Create который принимает JObject .

Класс JObject предоставляет средство для загрузки объекта JSON и обеспечивает доступ к данным внутри этого объекта.

ReadJson метод ReadJson создает объект JObject и вызывает метод Create (реализуемый нашим производным classом преобразователя), передавая экземпляр JObject .

Этот экземпляр JObject затем может быть проанализирован для определения правильного типа путем проверки наличия определенных полей.

пример

 string json = "[{ \"Department\": \"Department1\", \"JobTitle\": \"JobTitle1\", \"FirstName\": \"FirstName1\", \"LastName\": \"LastName1\" },{ \"Department\": \"Department2\", \"JobTitle\": \"JobTitle2\", \"FirstName\": \"FirstName2\", \"LastName\": \"LastName2\" }, {\"Skill\": \"Painter\", \"FirstName\": \"FirstName3\", \"LastName\": \"LastName3\" }]"; List persons = JsonConvert.DeserializeObject>(json, new PersonConverter()); ... public class PersonConverter : JsonCreationConverter { protected override Person Create(Type objectType, JObject jObject) { if (FieldExists("Skill", jObject)) { return new Artist(); } else if (FieldExists("Department", jObject)) { return new Employee(); } else { return new Person(); } } private bool FieldExists(string fieldName, JObject jObject) { return jObject[fieldName] != null; } } public abstract class JsonCreationConverter : JsonConverter { ///  /// Create an instance of objectType, based properties in the JSON object ///  /// type of object expected ///  /// contents of JSON object that will be deserialized ///  ///  protected abstract T Create(Type objectType, JObject jObject); public override bool CanConvert(Type objectType) { return typeof(T).IsAssignableFrom(objectType); } public override bool CanWrite { get { return false; } } 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 T target = Create(objectType, jObject); // Populate the object properties serializer.Populate(jObject.CreateReader(), target); return target; } } 

Вышеупомянутое решение для JsonCreationConverter находится по всему Интернету, но имеет недостаток, который проявляется в редких случаях. Новый JsonReader, созданный в методе ReadJson, не наследует ни одного из значений конфигурации исходного читателя (Culture, DateParseHandling, DateTimeZoneHandling, FloatParseHandling и т. Д.). Эти значения должны быть скопированы перед использованием нового JsonReader в serializer.Populate ().

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

Обновление. Я обновил это, чтобы иметь более явный метод, который делает копию существующего читателя. Это просто инкапсулирует процесс копирования по отдельным настройкам JsonReader. В идеале эта функция будет поддерживаться в самой библиотеке Newtonsoft, но на данный момент вы можете использовать следующее:

 /// Creates a new reader for the specified jObject by copying the settings /// from an existing reader. /// The reader whose settings should be copied. /// The jObject to create a new reader for. /// The new disposable reader. public static JsonReader CopyReaderForObject(JsonReader reader, JObject jObject) { JsonReader jObjectReader = jObject.CreateReader(); jObjectReader.Culture = reader.Culture; jObjectReader.DateFormatString = reader.DateFormatString; jObjectReader.DateParseHandling = reader.DateParseHandling; jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling; jObjectReader.FloatParseHandling = reader.FloatParseHandling; jObjectReader.MaxDepth = reader.MaxDepth; jObjectReader.SupportMultipleContent = reader.SupportMultipleContent; return jObjectReader; } 

Это следует использовать следующим образом:

 public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; // Load JObject from stream JObject jObject = JObject.Load(reader); // Create target object based on JObject T target = Create(objectType, jObject); // Populate the object properties using (JsonReader jObjectReader = CopyReaderForObject(reader, jObject)) { serializer.Populate(jObjectReader, target); } return target; } 

Предыдущее решение следует:

 /// Base Generic JSON Converter that can help quickly define converters for specific types by automatically /// generating the CanConvert, ReadJson, and WriteJson methods, requiring the implementer only to define a strongly typed Create method. public abstract class JsonCreationConverter : JsonConverter { /// Create an instance of objectType, based properties in the JSON object /// type of object expected /// contents of JSON object that will be deserialized protected abstract T Create(Type objectType, JObject jObject); /// Determines if this converted is designed to deserialization to objects of the specified type. /// The target type for deserialization. /// True if the type is supported. public override bool CanConvert(Type objectType) { // FrameWork 4.5 // return typeof(T).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); // Otherwise return typeof(T).IsAssignableFrom(objectType); } /// Parses the json to the specified type. /// Newtonsoft.Json.JsonReader /// Target type. /// Ignored /// Newtonsoft.Json.JsonSerializer to use. /// Deserialized Object public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; // Load JObject from stream JObject jObject = JObject.Load(reader); // Create target object based on JObject T target = Create(objectType, jObject); //Create a new reader for this jObject, and set all properties to match the original reader. JsonReader jObjectReader = jObject.CreateReader(); jObjectReader.Culture = reader.Culture; jObjectReader.DateParseHandling = reader.DateParseHandling; jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling; jObjectReader.FloatParseHandling = reader.FloatParseHandling; // Populate the object properties serializer.Populate(jObjectReader, target); return target; } /// Serializes to the specified type /// Newtonsoft.Json.JsonWriter /// Object to serialize. /// Newtonsoft.Json.JsonSerializer to use. public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { serializer.Serialize(writer, value); } } 

Просто подумал, что я бы поделился решением, основанным на этом, которое работает с атрибутом Knowntype с использованием рефлексии, должно было получить производный class из любого базового classа, решение может извлечь выгоду из рекурсии, чтобы найти лучший подходящий class, хотя мне это не нужно в моем case, соответствие выполняется типом, заданным для преобразователя, если у него есть KnownTypes, он будет сканировать их все до тех пор, пока он не будет соответствовать типу, который имеет все свойства внутри строки json, первый из которых будет соответствовать.

использование так же просто, как:

  string json = "{ Name:\"Something\", LastName:\"Otherthing\" }"; var ret = JsonConvert.DeserializeObject(json, new KnownTypeConverter()); 

в приведенном выше случае ret будет иметь тип B.

Класс JSON:

 [KnownType(typeof(B))] public class A { public string Name { get; set; } } public class B : A { public string LastName { get; set; } } 

Код конвертера:

 ///  /// Use KnownType Attribute to match a divierd class based on the class given to the serilaizer /// Selected class will be the first class to match all properties in the json object. ///  public class KnownTypeConverter : JsonConverter { public override bool CanConvert(Type objectType) { return System.Attribute.GetCustomAttributes(objectType).Any(v => v is KnownTypeAttribute); } 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 System.Attribute[] attrs = System.Attribute.GetCustomAttributes(objectType); // Reflection. // Displaying output. foreach (System.Attribute attr in attrs) { if (attr is KnownTypeAttribute) { KnownTypeAttribute k = (KnownTypeAttribute) attr; var props = k.Type.GetProperties(); bool found = true; foreach (var f in jObject) { if (!props.Any(z => z.Name == f.Key)) { found = false; break; } } if (found) { var target = Activator.CreateInstance(k.Type); serializer.Populate(jObject.CreateReader(),target); return target; } } } throw new ObjectNotFoundException(); // Populate the object properties } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); } } 

Это расширение для ответа тотема. Это в основном то же самое, но совпадение свойств основано на сериализованном объекте json, не отражающем объект .net. Это важно, если вы используете [JsonProperty], используя CamelCasePropertyNamesContractResolver или делаете что-нибудь еще, что приведет к тому, что json не будет соответствовать объекту .net.

Использование прост:

 [KnownType(typeof(B))] public class A { public string Name { get; set; } } public class B : A { public string LastName { get; set; } } 

Код конвертера:

 ///  /// Use KnownType Attribute to match a divierd class based on the class given to the serilaizer /// Selected class will be the first class to match all properties in the json object. ///  public class KnownTypeConverter : JsonConverter { public override bool CanConvert( Type objectType ) { return System.Attribute.GetCustomAttributes( objectType ).Any( v => v is KnownTypeAttribute ); } public override bool CanWrite { get { return false; } } 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 System.Attribute[ ] attrs = System.Attribute.GetCustomAttributes( objectType ); // Reflection. // check known types for a match. foreach( var attr in attrs.OfType( ) ) { object target = Activator.CreateInstance( attr.Type ); JObject jTest; using( var writer = new StringWriter( ) ) { using( var jsonWriter = new JsonTextWriter( writer ) ) { serializer.Serialize( jsonWriter, target ); string json = writer.ToString( ); jTest = JObject.Parse( json ); } } var jO = this.GetKeys( jObject ).Select( k => k.Key ).ToList( ); var jT = this.GetKeys( jTest ).Select( k => k.Key ).ToList( ); if( jO.Count == jT.Count && jO.Intersect( jT ).Count( ) == jO.Count ) { serializer.Populate( jObject.CreateReader( ), target ); return target; } } throw new SerializationException( string.Format( "Could not convert base class {0}", objectType ) ); } public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer ) { throw new NotImplementedException( ); } private IEnumerable> GetKeys( JObject obj ) { var list = new List>( ); foreach( var t in obj ) { list.Add( t ); } return list; } } 

В качестве другого варианта решения известного типа Тотема вы можете использовать reflection для создания универсального типа, чтобы избежать необходимости использовать известные атрибуты типа.

Это использует технику, похожую на GenericResolver Juval Lowy для WCF.

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

В моем собственном случае я решил использовать свойство $ type для обозначения типа в моем объекте json, а не пытаться определить его из свойств, хотя вы могли бы заимствовать у других решений здесь, чтобы использовать определение свойства.

  public class JsonKnownTypeConverter : JsonConverter { public IEnumerable KnownTypes { get; set; } public JsonKnownTypeConverter() : this(ReflectTypes()) { } public JsonKnownTypeConverter(IEnumerable 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 == x.Name)); } else { return Activator.CreateInstance(objectType); } 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(); } //Static helpers static Assembly CallingAssembly = Assembly.GetCallingAssembly(); static Type[] ReflectTypes() { List types = new List(); var referencedAssemblies = Assembly.GetExecutingAssembly().GetReferencedAssemblies(); foreach (var assemblyName in referencedAssemblies) { Assembly assembly = Assembly.Load(assemblyName); Type[] typesInReferencedAssembly = GetTypes(assembly); types.AddRange(typesInReferencedAssembly); } return types.ToArray(); } static Type[] GetTypes(Assembly assembly, bool publicOnly = true) { Type[] allTypes = assembly.GetTypes(); List types = new List(); foreach (Type type in allTypes) { if (type.IsEnum == false && type.IsInterface == false && type.IsGenericTypeDefinition == false) { if (publicOnly == true && type.IsPublic == false) { if (type.IsNested == false) { continue; } if (type.IsNestedPrivate == true) { continue; } } types.Add(type); } } return types.ToArray(); } 

Затем он может быть установлен как форматирование

 GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new JsonKnownTypeConverter()); 

Проект JsonSubTypes реализует универсальный конвертер, который обрабатывает эту функцию с помощью атрибутов.

Для конкретного примера, представленного здесь, как это работает:

  [JsonConverter(typeof(JsonSubtypes))] [JsonSubtypes.KnownSubTypeWithProperty(typeof(Employee), "JobTitle")] [JsonSubtypes.KnownSubTypeWithProperty(typeof(Artist), "Skill")] public class Person { public string FirstName { get; set; } public string LastName { get; set; } } public class Employee : Person { public string Department { get; set; } public string JobTitle { get; set; } } public class Artist : Person { public string Skill { get; set; } } [TestMethod] public void Demo() { string json = "[{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," + "{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," + "{\"Skill\":\"Painter\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}]"; var persons = JsonConvert.DeserializeObject>(json); Assert.AreEqual("Painter", (persons.Last() as Artist)?.Skill); } 

Вот еще одно решение, которое позволяет избежать использования jObject.CreateReader() и вместо этого создает новый JsonTextReader (который является поведением, используемым методом JsonCreate.Deserialze по умолчанию:

 public abstract class JsonCreationConverter : JsonConverter { protected abstract T Create(Type objectType, JObject jObject); public override bool CanConvert(Type objectType) { return typeof(T).IsAssignableFrom(objectType); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; // Load JObject from stream JObject jObject = JObject.Load(reader); // Create target object based on JObject T target = Create(objectType, jObject); // Populate the object properties StringWriter writer = new StringWriter(); serializer.Serialize(writer, jObject); using (JsonTextReader newReader = new JsonTextReader(new StringReader(writer.ToString()))) { newReader.Culture = reader.Culture; newReader.DateParseHandling = reader.DateParseHandling; newReader.DateTimeZoneHandling = reader.DateTimeZoneHandling; newReader.FloatParseHandling = reader.FloatParseHandling; serializer.Populate(newReader, target); } return target; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { serializer.Serialize(writer, value); } } 

Много раз реализация будет существовать в том же пространстве имен, что и интерфейс. Итак, я придумал это:

  public class InterfaceConverter : JsonConverter { public override bool CanWrite => false; public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var token = JToken.ReadFrom(reader); var typeVariable = this.GetTypeVariable(token); if (TypeExtensions.TryParse(typeVariable, out var implimentation)) { } else if (!typeof(IEnumerable).IsAssignableFrom(objectType)) { implimentation = this.GetImplimentedType(objectType); } else { var genericArgumentTypes = objectType.GetGenericArguments(); var innerType = genericArgumentTypes.FirstOrDefault(); if (innerType == null) { implimentation = typeof(IEnumerable); } else { Type genericType = null; if (token.HasAny()) { var firstItem = token[0]; var genericTypeVariable = this.GetTypeVariable(firstItem); TypeExtensions.TryParse(genericTypeVariable, out genericType); } genericType = genericType ?? this.GetImplimentedType(innerType); implimentation = typeof(IEnumerable<>); implimentation = implimentation.MakeGenericType(genericType); } } return JsonConvert.DeserializeObject(token.ToString(), implimentation); } public override bool CanConvert(Type objectType) { return !typeof(IEnumerable).IsAssignableFrom(objectType) && objectType.IsInterface || typeof(IEnumerable).IsAssignableFrom(objectType) && objectType.GetGenericArguments().Any(t => t.IsInterface); } protected Type GetImplimentedType(Type interfaceType) { if (!interfaceType.IsInterface) { return interfaceType; } var implimentationQualifiedName = interfaceType.AssemblyQualifiedName?.Replace(interfaceType.Name, interfaceType.Name.Substring(1)); return implimentationQualifiedName == null ? interfaceType : Type.GetType(implimentationQualifiedName) ?? interfaceType; } protected string GetTypeVariable(JToken token) { if (!token.HasAny()) { return null; } return token.Type != JTokenType.Object ? null : token.Value("$type"); } } 

Поэтому вы можете включить это глобально так:

 public static JsonSerializerSettings StandardSerializerSettings => new JsonSerializerSettings { Converters = new List { new InterfaceConverter() } }; 
  • Отключить JSON в существующий объект (Java)
  • VB.net JSON Deserialize
  • Библиотека Jackson JSON: как создать экземпляр classа, содержащего абстрактные поля
  • Десериализация JSON с использованием JSon.NET с динамическими данными
  • Уничтожить вложенные JSON в объекты C #
  • Newtonsoft JSON Deserialize
  • Преобразование int для bool с Json.Net
  • Сериализация Java - java.io.InvalidClassException локальный class несовместим
  • Как использовать JSON.NET для десериализации в вложенный / рекурсивный словарь и список?
  • Уничтожить XML-объект с помощью динамического
  • Давайте будем гением компьютера.