deserializeobject - jsonconvert c#
¿Cómo implementar JsonConverter personalizado en JSON.NET para deserializar una lista de objetos de clase base? (8)
Estoy tratando de extender el ejemplo de JSON.net que se encuentra aquí http://james.newtonking.com/projects/json/help/CustomCreationConverter.html
Tengo otra subclase derivada 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<Person> people = new List<Person>
{
new Employee(),
new Employee(),
new Artist(),
};
¿Cómo deserializo siguiendo a Json de nuevo a la Lista <Persona>
[
{
"Department": "Department1",
"JobTitle": "JobTitle1",
"FirstName": "FirstName1",
"LastName": "LastName1"
},
{
"Department": "Department2",
"JobTitle": "JobTitle2",
"FirstName": "FirstName2",
"LastName": "LastName2"
},
{
"Skill": "Painter",
"FirstName": "FirstName3",
"LastName": "LastName3"
}
]
No quiero usar TypeNameHandling JsonSerializerSettings. Estoy buscando específicamente la implementación personalizada de JsonConverter para manejar esto. La documentación y los ejemplos en torno a esto son bastante escasos en la red. Parece que no puedo entender correctamente la implementación del método ReadJson () anulado en JsonConverter.
Aquí hay otra solución que evita el uso de jObject.CreateReader()
, y en su lugar crea un nuevo JsonTextReader
(que es el comportamiento usado por el método predeterminado de JsonCreate.Deserialze
:
public abstract class JsonCreationConverter<T> : 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);
}
}
Como otra variación de la solución de tipo conocido de Totem, puede usar la reflexión para crear un resolutor de tipo genérico para evitar la necesidad de usar atributos de tipo conocido.
Esto utiliza una técnica similar a GenericResolver de Juval Lowy para WCF.
Mientras su clase base sea abstracta o una interfaz, los tipos conocidos se determinarán automáticamente en lugar de tener que ser decorados con atributos de tipo conocidos.
En mi propio caso, opté por usar una propiedad $ type para designar el tipo en mi objeto json en lugar de intentar determinarlo a partir de las propiedades, aunque podría solicitar un préstamo de otras soluciones aquí para usar la determinación basada en la propiedad.
public class JsonKnownTypeConverter : JsonConverter
{
public IEnumerable<Type> KnownTypes { get; set; }
public JsonKnownTypeConverter() : this(ReflectTypes())
{
}
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 == 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<Type> types = new List<Type>();
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<Type> types = new List<Type>();
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 un formateador.
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new JsonKnownTypeConverter());
El proyecto JsonSubTypes implementa un convertidor genérico que maneja esta característica con la ayuda de atributos.
Para la muestra concreta proporcionada 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<IReadOnlyCollection<Person>>(json);
Assert.AreEqual("Painter", (persons.Last() as Artist)?.Skill);
}
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á usando [JsonProperty], usando 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:
/// <summary>
/// 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.
/// </summary>
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<KnownTypeAttribute>( ) ) {
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<KeyValuePair<string, JToken>> GetKeys( JObject obj ) {
var list = new List<KeyValuePair<string, JToken>>( );
foreach( var t in obj ) {
list.Add( t );
}
return list;
}
}
La solución anterior para el JsonCreationConverter<T>
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 el serializador. Rellenar ().
Esto es lo mejor que se me ocurre 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 Actualicé esto para tener un método más explícito que haga una copia de un lector existente. Esto simplemente encapsula el proceso de copia sobre la configuración individual de JsonReader. Idealmente, esta función se mantendría en la propia biblioteca de Newtonsoft, pero por ahora, puede usar lo siguiente:
/// <summary>Creates a new reader for the specified jObject by copying the settings
/// from an existing reader.</summary>
/// <param name="reader">The reader whose settings should be copied.</param>
/// <param name="jObject">The jObject to create a new reader for.</param>
/// <returns>The new disposable reader.</returns>
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 utilizar 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:
/// <summary>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.</summary>
public abstract class JsonCreationConverter<T> : JsonConverter
{
/// <summary>Create an instance of objectType, based properties in the JSON object</summary>
/// <param name="objectType">type of object expected</param>
/// <param name="jObject">contents of JSON object that will be deserialized</param>
protected abstract T Create(Type objectType, JObject jObject);
/// <summary>Determines if this converted is designed to deserialization to objects of the specified type.</summary>
/// <param name="objectType">The target type for deserialization.</param>
/// <returns>True if the type is supported.</returns>
public override bool CanConvert(Type objectType)
{
// FrameWork 4.5
// return typeof(T).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
// Otherwise
return typeof(T).IsAssignableFrom(objectType);
}
/// <summary>Parses the json to the specified type.</summary>
/// <param name="reader">Newtonsoft.Json.JsonReader</param>
/// <param name="objectType">Target type.</param>
/// <param name="existingValue">Ignored</param>
/// <param name="serializer">Newtonsoft.Json.JsonSerializer to use.</param>
/// <returns>Deserialized Object</returns>
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;
}
/// <summary>Serializes to the specified type</summary>
/// <param name="writer">Newtonsoft.Json.JsonWriter</param>
/// <param name="value">Object to serialize.</param>
/// <param name="serializer">Newtonsoft.Json.JsonSerializer to use.</param>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
}
Muchas de las 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<string>("$type");
}
}
Por lo tanto, puedes incluir esto globalmente así:
public static JsonSerializerSettings StandardSerializerSettings => new JsonSerializerSettings
{
Converters = new List<JsonConverter>
{
new InterfaceConverter()
}
};
Simplemente pensé que compartiría una solución también basada en esto que funciona con el atributo Knowntype usando reflexión, tenía que obtener una clase derivada de cualquier clase base, la solución puede beneficiarse de la recursión para encontrar la mejor clase coincidente, aunque no la necesitaba en mi caso, la coincidencia se realiza 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á la primera que coincida.
El uso es tan simple como:
string json = "{ Name:/"Something/", LastName:/"Otherthing/" }";
var ret = JsonConvert.DeserializeObject<A>(json, new KnownTypeConverter());
En el caso anterior, ret será de 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:
/// <summary>
/// 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.
/// </summary>
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();
}
}
Usando el CustomCreationConverter
estándar, luchaba para trabajar en cómo generar el tipo correcto ( Person
o Employee
), porque para determinar esto necesita analizar el JSON y no hay una forma integrada de hacerlo usando el método Create
.
Encontré un hilo de discusión relacionado con la conversión de tipos y resultó proporcionar la respuesta. Aquí hay un enlace: Tipo de conversión .
Lo que se requiere es subclasificar JsonConverter
, anulando el método ReadJson
y creando un nuevo método de creación abstracto que acepte un objeto 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
invalidado crea un objeto JObject
e invoca el método Create
(implementado por nuestra clase de convertidor derivado), pasando 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<Person> persons =
JsonConvert.DeserializeObject<List<Person>>(json, new PersonConverter());
...
public class PersonConverter : JsonCreationConverter<Person>
{
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<T> : JsonConverter
{
/// <summary>
/// Create an instance of objectType, based properties in the JSON object
/// </summary>
/// <param name="objectType">type of object expected</param>
/// <param name="jObject">
/// contents of JSON object that will be deserialized
/// </param>
/// <returns></returns>
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;
}
}