¿Cómo implementar JsonConverter personalizado en JSON.NET para deserializar una Lista de objetos de clase base?

Estoy tratando de ampliar el ejemplo de JSON.net que se da aquí http://james.newtonking.com/projects/json/help/CustomCreationConverter.html

Tengo otra subclase que se deriva de la clase base / interfaz

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

¿Cómo deserializo siguiendo a Json de nuevo a la Lista

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

No quiero utilizar TypeNameHandling JsonSerializerSettings. Estoy buscando específicamente la implementación personalizada de JsonConverter para manejar esto. La documentación y los ejemplos a este respecto son bastante escasos en la red. Parece que no puedo obtener la implementación del método ReadJson () anulado en JsonConverter a la derecha.

Utilizando el CustomCreationConverter estándar, me CustomCreationConverter trabajo trabajar con la generación del tipo correcto ( Person o Employee ), porque para determinar esto es necesario analizar el JSON y no existe una manera integrada de hacerlo con el método Create .

Encontré un hilo de discusión relacionado con la conversión de tipo y resultó proporcionar la respuesta. Aquí hay un enlace: conversión de tipos .

Lo que se requiere es subclase JsonConverter , anulando el método ReadJson y creando un nuevo método abstracto Create que acepte un JObject .

La clase JObject proporciona un medio para cargar un objeto JSON y proporciona acceso a los datos dentro de este objeto.

El método ReadJson reemplazado crea un JObject e invoca el método Create (implementado por nuestra clase de convertidor derivado), pasando en la instancia de JObject .

Esta instancia de JObject se puede analizar para determinar el tipo correcto al verificar la existencia de ciertos campos.

Ejemplo

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

La solución anterior para JsonCreationConverter está en Internet, pero tiene un defecto que se manifiesta en raras ocasiones. El nuevo JsonReader creado en el método ReadJson no hereda ninguno de los valores de configuración del lector original (Culture, DateParseHandling, DateTimeZoneHandling, FloatParseHandling, etc …). Estos valores deben copiarse antes de usar el nuevo JsonReader en serializer.Populate ().

Esto es lo mejor que pude encontrar para solucionar algunos de los problemas con la implementación anterior, pero todavía creo que hay algunas cosas que se pasan por alto:

Actualizar lo actualicé para tener un método más explícito que hace una copia de un lector existente. Esto solo encapsula el proceso de copiar sobre la configuración individual de JsonReader. Idealmente, esta función se mantendría en la biblioteca de Newtonsoft misma, pero por ahora, puede usar lo siguiente:

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

Esto se debe usar de la siguiente manera:

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

Una solución más antigua sigue:

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

Solo pensé que compartiría una solución también basada en esto que funciona con el atributo Knowntype usando reflection, tuve que obtener clase derivada de cualquier clase base, la solución puede beneficiarse de la recursión para encontrar la mejor clase de coincidencia aunque no la necesitaba en mi caso, la coincidencia se hace por el tipo dado al convertidor si tiene Tipos Conocidos, los escaneará todos hasta que coincida con un tipo que tenga todas las propiedades dentro de la cadena json, se elegirá el primero que coincida.

el uso es tan simple como:

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

en el caso anterior, ret será del tipo B.

Clases JSON:

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

Código del convertidor:

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

Esta es una expansión a la respuesta del tótem. Básicamente, hace lo mismo, pero la coincidencia de propiedades se basa en el objeto json serializado, no refleja el objeto .net. Esto es importante si está utilizando [JsonProperty], utilizando CamelCasePropertyNamesContractResolver, o haciendo cualquier otra cosa que haga que el json no coincida con el objeto .net.

El uso es simple:

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

Código del convertidor:

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

Como otra variación de la solución de tipo conocido de Totem, puede usar la reflexión para crear un tipo de resolver genérico para evitar la necesidad de utilizar atributos de tipo conocidos.

Utiliza una técnica similar a GenericResolver de Juval Lowy para WCF.

Siempre que su clase base sea abstracta o una interfaz, los tipos conocidos se determinarán automáticamente en lugar de tener que decorarse con atributos de tipo conocidos.

En mi caso, opté por usar una propiedad $ type para designar el tipo en mi objeto json en lugar de tratar de determinarlo a partir de las propiedades, aunque podría tomar prestado de otras soluciones aquí para usar la determinación basada en la propiedad.

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

Luego se puede instalar como formateador

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

El proyecto JsonSubTypes implementa un convertidor genérico que maneja esta característica con ayuda de atributos.

Para la muestra de concreto provista aquí es cómo funciona:

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

Aquí hay otra solución que evita el uso de jObject.CreateReader() , y en su lugar crea un nuevo JsonTextReader (que es el comportamiento utilizado por el método JsonCreate.Deserialze predeterminado:

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

Muchas veces la implementación existirá en el mismo espacio de nombres que la interfaz. Entonces, se me ocurrió esto:

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

Por lo tanto, puede incluir esto globalmente así:

 public static JsonSerializerSettings StandardSerializerSettings => new JsonSerializerSettings { Converters = new List { new InterfaceConverter() } };