¿Cómo implementar JsonConverter personalizado en JSON.NET?
Estoy intentando ampliar el ejemplo de JSON.net que se proporciona 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 deserializa el siguiente Json y vuelvo a 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 utilizar TypeNameHandling JsonSerializerSettings. Estoy buscando específicamente una implementación JsonConverter personalizada para manejar esto. La documentación y los ejemplos sobre esto son bastante escasos en la red. Parece que no puedo entender correctamente la implementación del método ReadJson() anulado en JsonConverter.
Usando el estándar CustomCreationConverter
, estaba luchando por encontrar la forma de generar el tipo correcto ( Person
o Employee
), porque para determinar esto es necesario analizar el JSON y no existe una forma integrada de hacerlo usando el Create
método.
Encontré un hilo de discusión relacionado con la conversión de tipos y resultó proporcionar la respuesta. Aquí hay un enlace: Conversión de tipos (enlace archivado) .
Lo que se requiere es crear una subclase JsonConverter
, anular el ReadJson
método y crear un nuevo Create
método abstracto que acepte un archivo JObject
.
La clase JObject proporciona un medio para cargar un objeto JSON y proporciona acceso a los datos dentro de este objeto.
El ReadJson
método anulado crea JObject
e invoca el Create
método (implementado por nuestra clase convertidora derivada), pasando la JObject
instancia.
Luego, esta JObject
instancia se puede analizar para determinar el tipo correcto verificando 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;
}
}
La solución anterior está JsonCreationConverter<T>
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 se me ocurrió para solucionar algunos de los problemas con la implementación anterior, pero sigo pensando que se están pasando por alto algunas cosas:
Actualización Actualicé esto para tener un método más explícito que haga una copia de un lector existente. Esto simplemente resume el proceso de copiar configuraciones individuales de JsonReader. Lo ideal sería que esta función se mantuviera en la propia biblioteca de Newtonsoft, pero por ahora, puedes 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="jToken">The jToken to create a new reader for.</param>
/// <returns>The new disposable reader.</returns>
public static JsonReader CopyReaderForObject(JsonReader reader, JToken jToken)
{
JsonReader jTokenReader = jToken.CreateReader();
jTokenReader.Culture = reader.Culture;
jTokenReader.DateFormatString = reader.DateFormatString;
jTokenReader.DateParseHandling = reader.DateParseHandling;
jTokenReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
jTokenReader.FloatParseHandling = reader.FloatParseHandling;
jTokenReader.MaxDepth = reader.MaxDepth;
jTokenReader.SupportMultipleContent = reader.SupportMultipleContent;
return jTokenReader;
}
Esto debe usarse 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;
}
La solución anterior es la siguiente:
/// <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);
}
}
Solo pensé en compartir una solución también basada en esto que funciona con el atributo Knowntype usando la reflexión, tenía que obtener una clase derivada de cualquier clase base, la solución puede beneficiarse de la recursividad para encontrar la mejor clase coincidente, aunque no la necesitaba en mi En este caso, la coincidencia se realiza según el tipo proporcionado al convertidor, si tiene KnownTypes, 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<A>(json, new KnownTypeConverter());
en el caso anterior el 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:
/// <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();
}
}
El proyecto JsonSubTypes implementa un convertidor genérico que maneja esta característica con la ayuda de atributos.
Para el ejemplo concreto proporcionado aquí se explica 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 de 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], 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;
}
}
Como otra variación de la solución de tipos conocidos de Totem, puede utilizar la reflexión para crear un solucionador de tipos genérico para evitar la necesidad de utilizar atributos de tipos conocidos.
Esto utiliza una técnica similar al 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 decorarlos con atributos de tipos 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 prestadas otras soluciones aquí para usar la determinación basada en propiedades.
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 formateador.
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new JsonKnownTypeConverter());