JavaScriptSerializer.Deserialize – cómo cambiar los nombres de los campos

Resumen : ¿Cómo correlao un nombre de campo en datos JSON con un nombre de campo de un objeto .Net cuando uso JavaScriptSerializer.Deserialize?

Versión más larga : recibo los siguientes datos JSON de una API de servidor (No codificado en .Net)

{"user_id":1234, "detail_level":"low"} 

Tengo el siguiente objeto C # para él:

 [Serializable] public class DataObject { [XmlElement("user_id")] public int UserId { get; set; } [XmlElement("detail_level")] public DetailLevel DetailLevel { get; set; } } 

Donde DetailLevel es una enumeración con “Bajo” como uno de los valores.

Esta prueba falla:

 [TestMethod] public void DataObjectSimpleParseTest() { JavaScriptSerializer serializer = new JavaScriptSerializer(); DataObject dataObject = serializer.Deserialize(JsonData); Assert.IsNotNull(dataObject); Assert.AreEqual(DetailLevel.Low, dataObject.DetailLevel); Assert.AreEqual(1234, dataObject.UserId); } 

Y las dos últimas afirmaciones fallan, ya que no hay datos en esos campos. Si cambio los datos JSON a

  {"userid":1234, "detaillevel":"low"} 

Luego pasa. Pero no puedo cambiar el comportamiento del servidor, y quiero que las clases del cliente tengan propiedades bien nombradas en la expresión C #. No puedo usar LINQ to JSON porque quiero que funcione fuera de Silverlight. Parece que las tags XmlElement no tienen ningún efecto. No sé de dónde saqué la idea de que eran relevantes en absoluto, probablemente no.

¿Cómo se hace la asignación de nombre de campo en JavaScriptSerializer? ¿Se puede hacer?

Intenté otra vez, usando la clase DataContractJsonSerializer . Esto lo resuelve:

El código se ve así:

 using System.Runtime.Serialization; [DataContract] public class DataObject { [DataMember(Name = "user_id")] public int UserId { get; set; } [DataMember(Name = "detail_level")] public string DetailLevel { get; set; } } 

Y la prueba es:

 using System.Runtime.Serialization.Json; [TestMethod] public void DataObjectSimpleParseTest() { DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(DataObject)); MemoryStream ms = new MemoryStream(Encoding.Unicode.GetBytes(JsonData)); DataObject dataObject = serializer.ReadObject(ms) as DataObject; Assert.IsNotNull(dataObject); Assert.AreEqual("low", dataObject.DetailLevel); Assert.AreEqual(1234, dataObject.UserId); } 

El único inconveniente es que tuve que cambiar DetailLevel de una enumeración a una cadena; si mantienes el tipo de enumeración en su lugar, el DataContractJsonSerializer espera leer un valor numérico y falla. Ver DataContractJsonSerializer y Enumerados para más detalles.

En mi opinión, esto es bastante pobre, especialmente porque JavaScriptSerializer lo maneja correctamente. Esta es la excepción que obtiene tratando de analizar una cadena en una enumeración:

 System.Runtime.Serialization.SerializationException: There was an error deserializing the object of type DataObject. The value 'low' cannot be parsed as the type 'Int64'. ---> System.Xml.XmlException: The value 'low' cannot be parsed as the type 'Int64'. ---> System.FormatException: Input string was not in a correct format 

Y marcar la enumeración de esta manera no cambia este comportamiento:

 [DataContract] public enum DetailLevel { [EnumMember(Value = "low")] Low, ... } 

Esto también parece funcionar en Silverlight.

Al crear un JavaScriptConverter personalizado puede asignar cualquier nombre a cualquier propiedad. Pero requiere una encoding manual del mapa, que es menos que ideal.

 public class DataObjectJavaScriptConverter : JavaScriptConverter { private static readonly Type[] _supportedTypes = new[] { typeof( DataObject ) }; public override IEnumerable SupportedTypes { get { return _supportedTypes; } } public override object Deserialize( IDictionary dictionary, Type type, JavaScriptSerializer serializer ) { if( type == typeof( DataObject ) ) { var obj = new DataObject(); if( dictionary.ContainsKey( "user_id" ) ) obj.UserId = serializer.ConvertToType( dictionary["user_id"] ); if( dictionary.ContainsKey( "detail_level" ) ) obj.DetailLevel = serializer.ConvertToType( dictionary["detail_level"] ); return obj; } return null; } public override IDictionary Serialize( object obj, JavaScriptSerializer serializer ) { var dataObj = obj as DataObject; if( dataObj != null ) { return new Dictionary { {"user_id", dataObj.UserId }, {"detail_level", dataObj.DetailLevel } } } return new Dictionary(); } } 

Entonces puedes deserializar así:

 var serializer = new JavaScriptSerializer(); serialzer.RegisterConverters( new[]{ new DataObjectJavaScriptConverter() } ); var dataObj = serializer.Deserialize( json ); 

Json.NET hará lo que quieras. Admite la lectura de los atributos DataContract / DataMember, así como los propios, para cambiar los nombres de las propiedades. También existe la clase StringEnumConverter para serializar valores enum como el nombre en lugar del número.

No hay soporte estándar para cambiar el nombre de las propiedades en JavaScriptSerializer sin embargo, puede agregar fácilmente las suyas propias:

 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.Script.Serialization; using System.Reflection; public class JsonConverter : JavaScriptConverter { public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) { List members = new List(); members.AddRange(type.GetFields()); members.AddRange(type.GetProperties().Where(p => p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0)); object obj = Activator.CreateInstance(type); foreach (MemberInfo member in members) { JsonPropertyAttribute jsonProperty = (JsonPropertyAttribute)Attribute.GetCustomAttribute(member, typeof(JsonPropertyAttribute)); if (jsonProperty != null && dictionary.ContainsKey(jsonProperty.Name)) { SetMemberValue(serializer, member, obj, dictionary[jsonProperty.Name]); } else if (dictionary.ContainsKey(member.Name)) { SetMemberValue(serializer, member, obj, dictionary[member.Name]); } else { KeyValuePair kvp = dictionary.FirstOrDefault(x => string.Equals(x.Key, member.Name, StringComparison.InvariantCultureIgnoreCase)); if (!kvp.Equals(default(KeyValuePair))) { SetMemberValue(serializer, member, obj, kvp.Value); } } } return obj; } private void SetMemberValue(JavaScriptSerializer serializer, MemberInfo member, object obj, object value) { if (member is PropertyInfo) { PropertyInfo property = (PropertyInfo)member; property.SetValue(obj, serializer.ConvertToType(value, property.PropertyType), null); } else if (member is FieldInfo) { FieldInfo field = (FieldInfo)member; field.SetValue(obj, serializer.ConvertToType(value, field.FieldType)); } } public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { Type type = obj.GetType(); List members = new List(); members.AddRange(type.GetFields()); members.AddRange(type.GetProperties().Where(p => p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0)); Dictionary values = new Dictionary(); foreach (MemberInfo member in members) { JsonPropertyAttribute jsonProperty = (JsonPropertyAttribute)Attribute.GetCustomAttribute(member, typeof(JsonPropertyAttribute)); if (jsonProperty != null) { values[jsonProperty.Name] = GetMemberValue(member, obj); } else { values[member.Name] = GetMemberValue(member, obj); } } return values; } private object GetMemberValue(MemberInfo member, object obj) { if (member is PropertyInfo) { PropertyInfo property = (PropertyInfo)member; return property.GetValue(obj, null); } else if (member is FieldInfo) { FieldInfo field = (FieldInfo)member; return field.GetValue(obj); } return null; } public override IEnumerable SupportedTypes { get { return new[] { typeof(DataObject) }; } } } using System; using System.Collections.Generic; using System.Linq; using System.Text; [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public class JsonPropertyAttribute : Attribute { public JsonPropertyAttribute(string name) { Name = name; } public string Name { get; set; } } 

La clase DataObject se convierte en:

 public class DataObject { [JsonProperty("user_id")] public int UserId { get; set; } [JsonProperty("detail_level")] public DetailLevel DetailLevel { get; set; } } 

Considero que esto podría ser un poco tarde, pero pensé que otras personas que querían utilizar el JavaScriptSerializer lugar del DataContractJsonSerializer podrían apreciarlo.

Crea una clase heredada de JavaScriptConverter. A continuación, debe implementar tres cosas:

Métodos-

  1. Publicar por fascículos
  2. Deserializar

Propiedad-

  1. Tipos soportados

Puede usar la clase JavaScriptConverter cuando necesite más control sobre el proceso de serialización y deserialización.

 JavaScriptSerializer serializer = new JavaScriptSerializer(); serializer.RegisterConverters(new JavaScriptConverter[] { new MyCustomConverter() }); DataObject dataObject = serializer.Deserialize(JsonData); 

Aquí hay un enlace para más información

He usado el uso de Newtonsoft.Json como a continuación. Crea un objeto:

  public class WorklistSortColumn { [JsonProperty(PropertyName = "field")] public string Field { get; set; } [JsonProperty(PropertyName = "dir")] public string Direction { get; set; } [JsonIgnore] public string SortOrder { get; set; } } 

Ahora llame al siguiente método para serializar al objeto Json como se muestra a continuación.

 string sortColumn = JsonConvert.SerializeObject(worklistSortColumn); 

Para aquellos que no quieren ir a Newtonsoft Json.Net o DataContractJsonSerializer por alguna razón (no se me ocurre :)), aquí hay una implementación de JavaScriptConverter que admite DataContract y enum para la conversión de string

  public class DataContractJavaScriptConverter : JavaScriptConverter { private static readonly List _supportedTypes = new List(); static DataContractJavaScriptConverter() { foreach (Type type in Assembly.GetExecutingAssembly().DefinedTypes) { if (Attribute.IsDefined(type, typeof(DataContractAttribute))) { _supportedTypes.Add(type); } } } private bool ConvertEnumToString = false; public DataContractJavaScriptConverter() : this(false) { } public DataContractJavaScriptConverter(bool convertEnumToString) { ConvertEnumToString = convertEnumToString; } public override IEnumerable SupportedTypes { get { return _supportedTypes; } } public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) { if (Attribute.IsDefined(type, typeof(DataContractAttribute))) { try { object instance = Activator.CreateInstance(type); IEnumerable members = ((IEnumerable)type.GetFields()) .Concat(type.GetProperties().Where(property => property.CanWrite && property.GetIndexParameters().Length == 0)) .Where((member) => Attribute.IsDefined(member, typeof(DataMemberAttribute))); foreach (MemberInfo member in members) { DataMemberAttribute attribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute)); object value; if (dictionary.TryGetValue(attribute.Name, out value) == false) { if (attribute.IsRequired) { throw new SerializationException(String.Format("Required DataMember with name {0} not found", attribute.Name)); } continue; } if (member.MemberType == MemberTypes.Field) { FieldInfo field = (FieldInfo)member; object fieldValue; if (ConvertEnumToString && field.FieldType.IsEnum) { fieldValue = Enum.Parse(field.FieldType, value.ToString()); } else { fieldValue = serializer.ConvertToType(value, field.FieldType); } field.SetValue(instance, fieldValue); } else if (member.MemberType == MemberTypes.Property) { PropertyInfo property = (PropertyInfo)member; object propertyValue; if (ConvertEnumToString && property.PropertyType.IsEnum) { propertyValue = Enum.Parse(property.PropertyType, value.ToString()); } else { propertyValue = serializer.ConvertToType(value, property.PropertyType); } property.SetValue(instance, propertyValue); } } return instance; } catch (Exception) { return null; } } return null; } public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { Dictionary dictionary = new Dictionary(); if (obj != null && Attribute.IsDefined(obj.GetType(), typeof(DataContractAttribute))) { Type type = obj.GetType(); IEnumerable members = ((IEnumerable)type.GetFields()) .Concat(type.GetProperties().Where(property => property.CanRead && property.GetIndexParameters().Length == 0)) .Where((member) => Attribute.IsDefined(member, typeof(DataMemberAttribute))); foreach (MemberInfo member in members) { DataMemberAttribute attribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute)); object value; if (member.MemberType == MemberTypes.Field) { FieldInfo field = (FieldInfo)member; if (ConvertEnumToString && field.FieldType.IsEnum) { value = field.GetValue(obj).ToString(); } else { value = field.GetValue(obj); } } else if (member.MemberType == MemberTypes.Property) { PropertyInfo property = (PropertyInfo)member; if (ConvertEnumToString && property.PropertyType.IsEnum) { value = property.GetValue(obj).ToString(); } else { value = property.GetValue(obj); } } else { continue; } if (dictionary.ContainsKey(attribute.Name)) { throw new SerializationException(String.Format("More than one DataMember found with name {0}", attribute.Name)); } dictionary[attribute.Name] = value; } } return dictionary; } } 

Nota: Este DataContractJavaScriptConverter solo manejará las clases de DataContract definidas en el ensamblaje donde se coloca. Si desea clases de ensamblajes separados, modifique la lista _supportedTypes en consecuencia en el constructor estático.

Esto se puede usar de la siguiente manera:

  JavaScriptSerializer serializer = new JavaScriptSerializer(); serializer.RegisterConverters(new JavaScriptConverter[] { new DataContractJavaScriptConverter(true) }); DataObject dataObject = serializer.Deserialize(JsonData); 

La clase DataObject se vería así:

  using System.Runtime.Serialization; [DataContract] public class DataObject { [DataMember(Name = "user_id")] public int UserId { get; set; } [DataMember(Name = "detail_level")] public string DetailLevel { get; set; } } 

Tenga en cuenta que esta solución no maneja las propiedades EmitDefaultValue y Order compatibles con el atributo DataMember .

Mis requisitos incluyen:

  • debe respetar los datosContratos
  • debe deserializar las fechas en el formato recibido en servicio
  • debe manejar colelctions
  • debe apuntar 3.5
  • NO debe agregar una dependencia externa, especialmente no Newtonsoft (estoy creando un paquete distribuible)
  • no debe ser deserializado a mano

Mi solución al final fue usar SimpleJson ( https://github.com/facebook-csharp-sdk/simple-json ).

Aunque puedes instalarlo a través de un paquete nuget, incluí ese único archivo SimpleJson.cs (con la licencia MIT) en mi proyecto y lo hice referencia.

Espero que esto ayude a alguien.