JavaScriptSerializer - custom property name

2020-01-29 16:02发布

I am using JavaScriptSerializer to deserialize json data. Everything works pretty well, but my problem is, that one property in json data is named 'base', so I cannot create such property in my C# code. I found that i can manually map values to properties in constructor, but the issue is, that my DTOs have like 200 properties, so I do not want to make this manually and would prefer to find any other solution. I also Tried to use annotations, but this:

[JsonProperty("base")]
public int baseValue { get; set; }

did not help me, value baseValue was set to 0 each time (if you think, that this annotation should work, I can post my whole code, not only this 2 lines)

Is there any way how could I simply solve my issue?

2条回答
时光不老,我们不散
2楼-- · 2020-01-29 16:34

As dbc wrote, JavaScriptSerializer is very limited. Since we cannot use Json.NET in our project, I wrote a JavaScriptConverter called CustomJavaScriptSerializer to enhance JavaScriptSerializer.

Unfortunately, because of the way JavaScriptConverter and JavaScriptSerializer work together (that could have been done better, Microsoft!), it is necessary that you derive your class to be serialized from CustomJavaScriptSerializer. That's the only limitation.

But then you have full control and flexibility over how your class is serialized/deserialized. Some handy features are already bulit in, like partial support for JsonProperty or lower-casing the first letter of all property names (as it is convention in JavaScript). For exact usage and all features, see comments in the code. In addition to this, you can override serialization methods in any of your derived classes to fine-tune serialization specific to that particular class.

Though I think the class is very reliable, of course, as always, I do not take over any liability of any kind. Use on your own risk.

That said, here's the code:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Web.Script.Serialization;

namespace SomeNamespace
{
    #region Class CustomJavaScriptSerializer

    /// <summary>
    /// Custom JavaScript serializer, see https://msdn.microsoft.com/en-us/library/system.web.script.serialization.javascriptconverter%28v=vs.110%29.aspx
    /// </summary>
    /// <remarks>
    /// Used to enhance functionality of standard <see cref="System.Web.Script.Serialization.JavaScriptSerializer"/>. 
    /// E.g. to support <see cref="JsonPropertyAttribute"/> that provides some properties known from Newtonsoft's JavaScript serializer, 
    /// see https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_JsonPropertyAttribute.htm.
    /// Additionally, there is an attribute <see cref="JsonClassAttribute"/> that can be applied to the class 
    /// to manipulate JSON serialization behavior of all properties of the class.
    /// Use this JSON serializer when including Newtonsoft's JavaScript serializer is not an option for you.
    /// 
    /// Usage: 
    /// 
    ///  - Just derive your class to be JSON serialized / deserialized from this class. 
    ///  - You now can decorate the properties of your class with e.g. [JsonProperty( "someName" )]. See <see cref="JsonPropertyAttribute"/> for details.
    ///  - Additionally, you can decorate your class with e.g. [JsonClass( DoNotLowerCaseFirstLetter = true)]. See <see cref="JsonClassAttribute"/> for details.
    ///  - Important! Use static methods <see cref="JsonSerialize"/> and <see cref="JsonDeserialize"/> of this class for JSON serialization / deserialization.
    ///  - For further customization specific to your class, you can override <see cref="GetSerializedProperty"/> and <see cref="SetDeserializedProperty"/> in your derived class.
    ///   
    /// Example:
    /// 
    /// <![CDATA[
    /// 
    /// [JsonClass( DoNotLowerCaseFirstLetter = true )]
    /// public class SomeClass: CustomJavaScriptSerializer
    /// {
    ///     
    ///     /// <summary>
    ///     /// The tooltip. Will be transferred to JavaScript as "tooltext".
    ///     /// </summary>
    ///     [JsonProperty( "tooltext" )]
    ///     public string Tooltip
    ///     {
    ///         get;
    ///         set;
    ///     }
    ///     
    ///     ...
    /// }
    /// 
    /// ]]>
    /// </remarks>
    public abstract class CustomJavaScriptSerializer : JavaScriptConverter
    {
        #region Fields

        /// <summary>
        /// Dictionary to collect all derived <see cref="CustomJavaScriptSerializer"/> to be registered with <see cref="JavaScriptConverter.RegisterConverters"/>
        /// </summary>
        private static Dictionary<Type, CustomJavaScriptSerializer> convertersToRegister = new Dictionary<Type, CustomJavaScriptSerializer>();

        /// <summary>
        /// Sync for <see cref="convertersToRegister"/>.
        /// </summary>
        private static readonly object sync = new object();

        #endregion

        #region Properties

        /// <summary>
        /// All derived <see cref="CustomJavaScriptSerializer"/> to be registered with <see cref="JavaScriptConverter.RegisterConverters"/>
        /// </summary>
        public static IEnumerable<CustomJavaScriptSerializer> ConvertersToRegister
        {
            get
            {
                return CustomJavaScriptSerializer.convertersToRegister.Values;
            }
        }

        /// <summary>
        /// <see cref="JsonClassAttribute"/> retrieved from decoration of the derived class.
        /// </summary>
        /// <remarks>
        /// Is only set when an instance of this class is used for serialization. See constructor for details.
        /// </remarks>
        protected JsonClassAttribute ClassAttribute
        {
            get;
            private set;
        }

        #endregion

        #region Constructors

        /// <summary>
        /// Default constructor
        /// </summary>
        public CustomJavaScriptSerializer()
        {
            Type type = this.GetType();

            if ( CustomJavaScriptSerializer.convertersToRegister.ContainsKey( type ) )
                return;

            lock( sync )
            {
                // Remember this CustomJavaScriptSerializer instance to do serialization for this type.
                if ( CustomJavaScriptSerializer.convertersToRegister.ContainsKey( type ) )
                    return;

                // Performance: Set ClassAttribute only for the instance used for serialization.
                this.ClassAttribute = ( this.GetType().GetCustomAttributes( typeof( JsonClassAttribute ), true ).FirstOrDefault() as JsonClassAttribute ) ?? new JsonClassAttribute();
                convertersToRegister[ type ] = this;
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Converts <paramref name="obj"/> to a JSON string.
        /// </summary>
        /// <param name="obj">The object to serialize.</param>
        /// <returns>The serialized JSON string.</returns>
        public static string JsonSerialize( object obj )
        {
            var serializer = new JavaScriptSerializer();
            serializer.RegisterConverters( CustomJavaScriptSerializer.ConvertersToRegister );
            serializer.MaxJsonLength = int.MaxValue;
            return serializer.Serialize( obj );
        }

        /// <summary>
        /// Converts a JSON-formatted string to an object of the specified type.
        /// </summary>
        /// <param name="input">The JSON string to deserialize.</param>
        /// <param name="targetType">The type of the resulting object.</param>
        /// <returns>The deserialized object.</returns>
        public static object JsonDeserialize( string input, Type targetType )
        {
            var serializer = new JavaScriptSerializer();
            serializer.RegisterConverters( CustomJavaScriptSerializer.ConvertersToRegister );
            serializer.MaxJsonLength = int.MaxValue;
            return serializer.Deserialize( input, targetType );
        }

        /// <summary>
        /// Converts the specified JSON string to an object of type <typeparamref name="T"/>.
        /// </summary>
        /// <typeparam name="T">The type of the resulting object.</typeparam>
        /// <param name="input">The JSON string to be deserialized.</param>
        /// <returns>The deserialized object.</returns>
        public static T JsonDeserialize<T>( string input )
        {
            return (T)CustomJavaScriptSerializer.JsonDeserialize( input, typeof( T ) );
        }

        /// <summary>
        /// Get this object serialized as JSON string.
        /// </summary>
        /// <returns>This object as JSON string.</returns>
        public string ToJson()
        {
            return CustomJavaScriptSerializer.JsonSerialize( this );
        }

        #endregion

        #region Overrides

        /// <summary>
        /// Return list of supported types. This is just a derived class here.
        /// </summary>
        [ScriptIgnore]
        public override IEnumerable<Type> SupportedTypes
        {
            get
            {
                return new ReadOnlyCollection<Type>( new List<Type>(){ this.GetType() } );
            }
        }

        /// <summary>
        /// Serialize the passed <paramref name="obj"/>.
        /// </summary>
        /// <param name="obj">The object to serialize.</param>
        /// <param name="serializer">The <see cref="JavaScriptSerializer"/> calling this method.</param>
        /// <returns>A dictionary with name value pairs representing property name value pairs as they shall appear in JSON. </returns>
        public override IDictionary<string, object> Serialize( object obj, JavaScriptSerializer serializer )
        {
            var result = new Dictionary<string, object>();

            if ( obj == null )
                return result;

            BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;

            PropertyInfo[] properties = this.GetType().GetProperties( bindingFlags );

            foreach ( PropertyInfo property in properties )
            {
                KeyValuePair<string, object> kvp = this.GetSerializedProperty( obj, property );
                if ( !string.IsNullOrEmpty( kvp.Key ) )
                    result[ kvp.Key ] = kvp.Value;
            }

            return result;
        }

        /// <summary>
        /// Deserialize <paramref name="dictionary"/> to <paramref name="type"/>.
        /// </summary>
        /// <remarks>
        /// Reverse method to <see cref="Serialize"/>
        /// </remarks>
        /// <param name="dictionary">The dictionary to be deserialized.</param>
        /// <param name="type">Type to deserialize to. This is the type of the derived class, see <see cref="SupportedTypes"/>.</param>
        /// <param name="serializer">The <see cref="JavaScriptSerializer"/> calling this method.</param>
        /// <returns>An object of type <paramref name="type"/> with property values set from <paramref name="dictionary"/>.</returns>
        public override object Deserialize( IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer )
        {
            if ( dictionary == null )
                throw new ArgumentNullException( "dictionary" );

            if ( type == null )
                throw new ArgumentNullException( "type" );

            if ( serializer == null )
                throw new ArgumentNullException( "serializer" );

            // This will fail if type has no default constructor.
            object result = Activator.CreateInstance( type );

            BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;

            PropertyInfo[] properties = this.GetType().GetProperties( bindingFlags );

            foreach ( PropertyInfo property in properties )
            {
                this.SetDerializedProperty( result, property, dictionary, serializer );
            }

            return result;
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// Get a key value pair as base for serialization, based on the passed <paramref name="property"/>.
        /// </summary>
        /// <remarks>
        /// The returned <see cref="KeyValuePair"/> key represents the property's name in JSON, the value its value.
        /// </remarks>
        /// <param name="obj">The object to serialize.</param>
        /// <param name="property">The <see cref="PropertyInfo"/>To be serialized.</param>
        /// <returns>The requested key value pair or an empty key value pair (key = null) to ignore this property.</returns>
        protected virtual KeyValuePair<string, object> GetSerializedProperty( object obj, PropertyInfo property )
        {
            var result = new KeyValuePair<string, object>();

            if ( property == null || !property.CanRead )
                return result;

            object value = property.GetValue( obj );
            if ( value == null && !this.ClassAttribute.SerializeNullValues )
                return result;

            JsonPropertyAttribute jsonPropertyAttribute = this.GetJsonPropertyAttribute( property );
            if ( jsonPropertyAttribute == null || jsonPropertyAttribute.Ignored )
                return result;

            if ( value != null && jsonPropertyAttribute.UseToString )
                value = value.ToString();

            string name = jsonPropertyAttribute.PropertyName;
            return new KeyValuePair<string, object>( name, value );
        }

        /// <summary>
        /// Set <paramref name="property"/> of <paramref name="obj"/> with value provided in <paramref name="dictionary"/>.
        /// </summary>
        /// <param name="obj">The object to set <paramref name="property of"/>.</param>
        /// <param name="property">The property to set its value.</param>
        /// <param name="dictionary">Dictionary with property name - value pairs to query for <paramref name="property"/> value.</param>
        /// <param name="serializer">The <see cref="JavaScriptSerializer"/> calling this method.</param>
        public virtual void SetDerializedProperty( object obj, PropertyInfo property, IDictionary<string, object> dictionary, JavaScriptSerializer serializer )
        {
            if ( obj == null || property == null || !property.CanWrite || dictionary == null || serializer == null )
                return;

            JsonPropertyAttribute jsonPropertyAttribute = this.GetJsonPropertyAttribute( property );
            if ( jsonPropertyAttribute == null || jsonPropertyAttribute.Ignored || jsonPropertyAttribute.UseToString )
                return;

            string name = jsonPropertyAttribute.PropertyName;
            if ( !dictionary.ContainsKey( name ) )
                return;

            object value = dictionary[ name ];

            // Important! Use JavaScriptSerializer.ConvertToType so that CustomJavaScriptSerializers of properties of this class are called recursively. 
            object convertedValue = serializer.ConvertToType( value, property.PropertyType );
            property.SetValue( obj, convertedValue );
        }

        /// <summary>
        /// Gets a <see cref="JsonPropertyAttribute"/> for the passed <see cref="PropertyInfo"/>.
        /// </summary>
        /// <param name="property">The property to examine. May not be null.</param>
        /// <returns>A <see cref="JsonPropertyAttribute"/> with properties set to be used directly as is, never null.</returns>
        protected JsonPropertyAttribute GetJsonPropertyAttribute( PropertyInfo property )
        {
            if ( property == null )
                throw new ArgumentNullException( "property" );

            object[] attributes = property.GetCustomAttributes( true );

            JsonPropertyAttribute jsonPropertyAttribute = null;
            bool ignore = false;

            foreach ( object attribute in attributes )
            {
                if ( attribute is ScriptIgnoreAttribute )
                    ignore = true;

                if ( attribute is JsonPropertyAttribute )
                    jsonPropertyAttribute = (JsonPropertyAttribute)attribute;
            }

            JsonPropertyAttribute result = jsonPropertyAttribute ?? new JsonPropertyAttribute();
            result.Ignored |= ignore;

            if ( string.IsNullOrWhiteSpace( result.PropertyName ) )
                result.PropertyName = property.Name;

            if ( !this.ClassAttribute.DoNotLowerCaseFirstLetter && ( jsonPropertyAttribute == null || string.IsNullOrWhiteSpace( jsonPropertyAttribute.PropertyName ) ) )
            {
                string name = result.PropertyName.Substring( 0, 1 ).ToLowerInvariant();
                if ( result.PropertyName.Length > 1 )
                    name += result.PropertyName.Substring( 1 );
                result.PropertyName = name;
            }

            return result;
        }

        #endregion
    }

    #endregion

    #region Class JsonClassAttribute

    /// <summary>
    /// Attribute to be used in conjunction with <see cref="CustomJavaScriptSerializer"/>.
    /// </summary>
    /// <remarks>
    /// Decorate your class derived from <see cref="CustomJavaScriptSerializer"/> with this attribute to 
    /// manipulate how JSON serialization / deserialization is done for all properties of your derived class.
    /// </remarks>
    [AttributeUsage( AttributeTargets.Class )]
    public class JsonClassAttribute : Attribute
    {
        #region Properties

        /// <summary>
        /// By default, all property names are automatically converted to have their first letter lower case (as it is convention in JavaScript). Set this to true to avoid that behavior.
        /// </summary>
        public bool DoNotLowerCaseFirstLetter
        {
            get;
            set;
        }

        /// <summary>
        /// By default, properties with value null are not serialized. Set this to true to avoid that behavior.
        /// </summary>
        public bool SerializeNullValues
        {
            get;
            set;
        }

        #endregion
    }

    #endregion

    #region Class JsonPropertyAttribute

    /// <summary>
    /// Attribute to be used in conjunction with <see cref="CustomJavaScriptSerializer"/>.
    /// </summary>
    /// <remarks>
    /// Among others, used to define a property's name when being serialized to JSON. 
    /// Implements some functionality found in Newtonsoft's JavaScript serializer, 
    /// see https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_JsonPropertyAttribute.htm
    /// </remarks>
    [AttributeUsage( AttributeTargets.Property )]
    public class JsonPropertyAttribute : Attribute
    {
        #region Properties

        /// <summary>
        /// True to ignore this property.
        /// </summary>
        public bool Ignored
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the name of the property. 
        /// </summary>
        public string PropertyName
        {
            get;
            set;
        }

        /// <summary>
        /// When true, the value of this property is serialized using value.ToString().
        /// </summary>
        /// <remarks>
        /// Can be handy when serializing e.g. enums or types.
        /// Do not set this to true when deserialization is needed, since there is no general inverse method to ToString().
        /// When this is true, the property is just ignored when deserializing.
        /// </remarks>
        public bool UseToString
        {
            get;
            set;
        }

        #endregion

        #region Constructors

        /// <summary>
        /// Default constructor
        /// </summary>
        public JsonPropertyAttribute()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="JsonPropertyAttribute"/>  class with the specified name. 
        /// </summary>
        /// <param name="propertyName">Name of the property</param>
        public JsonPropertyAttribute( string propertyName )
        {
            this.PropertyName = propertyName;
        }

        #endregion
    }

    #endregion
}

And a unit test:

#region CustomJavaScriptSerializer

/// <summary>Tests behavior of CustomJavaScriptSerializer.</summary>
[TestMethod]
public void TestCustomJavaScriptSerializer()
{
    // 11
    var dataItem11 = new JsonSerializeTest1();
    dataItem11.Label = "LabelName";
    dataItem11.Value = 5;
    dataItem11.Test = TestEnum.B;
    dataItem11.Tooltip = "TooltipName";

    string json11 = dataItem11.ToJson();
    Assert.IsTrue( json11 == "{\"label\":\"LabelName\",\"value\":5,\"test\":2,\"tooltext\":\"TooltipName\"}" );

    JsonSerializeTest1 deserialized11 = CustomJavaScriptSerializer.JsonDeserialize<JsonSerializeTest1>( json11 );
    Assert.IsNotNull( deserialized11 );
    Assert.IsTrue( deserialized11.Equals( dataItem11 ) );

    // 12
    var dataItem12 = new JsonSerializeTest1();
    dataItem12.Value = 5;
    dataItem12.Test = TestEnum.A;
    dataItem12.Tooltip = "TooltipName";

    string json12 = dataItem12.ToJson();
    Assert.IsTrue( json12 == "{\"value\":5,\"test\":1,\"tooltext\":\"TooltipName\"}" );

    JsonSerializeTest1 deserialized12 = CustomJavaScriptSerializer.JsonDeserialize<JsonSerializeTest1>( json12 );
    Assert.IsNotNull( deserialized12 );
    Assert.IsTrue( deserialized12.Equals( dataItem12 ) );

    // 21
    var dataItem21 = new JsonSerializeTest2();
    dataItem21.Label = "LabelName";
    dataItem21.Value = 5;
    dataItem21.Test = TestEnum.B;
    dataItem21.Tooltip = "TooltipName";

    string json21 = dataItem21.ToJson();
    Assert.IsTrue( json21 == "{\"Test\":\"B\",\"Label\":\"LabelName\",\"Value\":5}" );

    JsonSerializeTest2 deserialized21 = CustomJavaScriptSerializer.JsonDeserialize<JsonSerializeTest2>( json21 );
    Assert.IsNotNull( deserialized21 );
    Assert.IsTrue( deserialized21.Label == "LabelName" );
    Assert.IsTrue( deserialized21.Value == 5 );
    // No mistake! UseToString = true here. See JsonPropertyAttribute.UseToString. 
    Assert.IsTrue( deserialized21.Test == 0 );
    Assert.IsTrue( deserialized21.Tooltip == null );

    // 22
    var dataItem22 = new JsonSerializeTest2();
    dataItem22.Tooltip = "TooltipName";

    string json22 = dataItem22.ToJson();
    Assert.IsTrue( json22 == "{\"Test\":\"0\",\"Label\":null,\"Value\":null}" );

    JsonSerializeTest2 deserialized22 = CustomJavaScriptSerializer.JsonDeserialize<JsonSerializeTest2>( json22 );
    Assert.IsNotNull( deserialized22 );
    Assert.IsTrue( deserialized22.Label == null );
    Assert.IsTrue( deserialized22.Value == null );
    Assert.IsTrue( deserialized22.Test == 0 );
    Assert.IsTrue( deserialized22.Tooltip == null );

    var list = new List<JsonSerializeTest1>() { dataItem11, dataItem12 };
    var json = CustomJavaScriptSerializer.JsonSerialize( list );
    List<JsonSerializeTest1> deserialized = CustomJavaScriptSerializer.JsonDeserialize<List<JsonSerializeTest1>>( json );
    Assert.IsNotNull( deserialized );
    Assert.IsTrue( deserialized.Count == 2 );
    Assert.IsTrue( deserialized[ 0 ].Equals( deserialized11 ) );
    Assert.IsTrue( deserialized[ 1 ].Equals( deserialized12 ) );
}


[JsonClass( DoNotLowerCaseFirstLetter = true, SerializeNullValues = true )]
public class JsonSerializeTest2 : JsonSerializeTest1
{
    /// <summary>
    /// A tooltip
    /// </summary>
    [JsonProperty( Ignored = true )]
    public override string Tooltip
    {
        get;
        set;
    }

    /// <summary>
    /// An enum
    /// </summary>
    [JsonProperty( UseToString = true )]
    public override TestEnum Test
    {
        get;
        set;
    }
}

public class JsonSerializeTest1 : CustomJavaScriptSerializer
{
    /// <summary>
    /// A label
    /// </summary>
    public virtual string Label
    {
        get;
        set;
    }

    /// <summary>
    /// A Value
    /// </summary>
    public virtual decimal? Value
    {
        get;
        set;
    }

    /// <summary>
    /// An enum
    /// </summary>
    public virtual TestEnum Test
    {
        get;
        set;
    }

    /// <summary>
    /// A tooltip
    /// </summary>
    [JsonProperty( "tooltext" )]
    public virtual string Tooltip
    {
        get;
        set;
    }

    /// <summary>
    /// Whether this object is the same as <paramref name="obj"/>.
    /// </summary>
    /// <returns>True = <paramref name="obj"/> is the same as this object, false otherwise.</returns>
    public override bool Equals( object obj )
    {
        var other = obj as JsonSerializeTest1;

        // Cast to object to avoid that it calls overridden == operator here.
        if ( (object)other == null )
            return false;

        return this.Label == other.Label && this.Value == other.Value && this.Test == other.Test && this.Tooltip == other.Tooltip;
    }

    /// <summary>
    /// Get hash code for comparison
    /// </summary>
    public override int GetHashCode()
    {
        return base.GetHashCode();
    }
}

public enum TestEnum
{
    A = 1,
    B = 2
}

#endregion

Enjoy!

查看更多
我想做一个坏孩纸
3楼-- · 2020-01-29 16:38

Answering in several parts:

  1. To make a property named base, you need to prefix the name with an @:

    public int @base { get; set; }
    
  2. You wrote that you are using JavaScriptSerializer. The attribute [JsonProperty] is for a completely different serializer, Json.NET. This attribute has no effect on JavaScriptSerializer.

    If you were to switch to Json.NET, you would be able to use this attribute.

    Or, if you were to instead apply data contract attributes to your type, you could use either Json.NET or DataContractJsonSerializer to serialize your type with renamed properties.

  3. In fact, JavaScriptSerializer has no way to rename a property for serialization outside of writing a custom JavaScriptConverter. This serializer is quite bare-bones; the only serialization attribute it supports is ScriptIgnore to suppress serialization of a property.

查看更多
登录 后发表回答