Deserialize a json field with different data types

2019-08-24 05:02发布

问题:

I have an issue while deserializing json data which can have both float or array type of data. The same issue from here Dealing with JSON field that holds different types in C#

But everywhere the solution is to use json.net with a JsonConverter. I need to achieve the deserialization using only System.Web.Script.Serialization.JavaScriptSerializer in c#. Can anyone help, pls?

回答1:

You can use a JavaScriptConverter for this purpose. However, unlike Json.NET's JsonConverter a JavaScriptConverter can only be used for types that map from and to a JSON object -- not an array or primitive type. Thus you will need to create a custom converter for any object that may contain a polymorphic property that could be an array or singleton item.

Let's imagine you have JSON that looks like the following:

{
  "name": "my name",
  "data": {
    "foo": "Foo",
    "bar": "Bar"
  },
  "values": [
    3.14,
    2.718
  ]
}

Where "values" might sometimes be a primitive value like so:

  "values": 3.14

And, you want to map this to the following POCO:

public class RootObject
{
    public string name { get; set; }
    public NestedData data { get; set; }
    public float[] Values { get; set; }
}

public class NestedData
{
    public string foo { get; set; }
    public string bar { get; set; }
}

As JavaScriptConverter.Deserialize() is passed an IDictionary<string, object> of parsed values, the steps to take are:

  1. Detach any properties that need custom processing (keeping in mind that JavaScriptSerializer is case-insensitive but that the dictionary is not).

  2. Generate a default deserialization for any remaining properties using JavaScriptSerializer.ConvertToType<T>() using a fresh serializer that does not contain the converter.

  3. Manually deserialize and populate the custom properties into the partially deserialized object, and return it.

For the type shown above, the following converter, based somewhat on this answer, does the job:

class RootObjectConverter : CustomPropertiesConverter<RootObject>
{
    const string ValuesName = "values";

    protected override IEnumerable<string> CustomProperties
    {
        get { return new[] { ValuesName }; }
    }

    protected override void DeserializeCustomProperties(Dictionary<string, object> customDictionary, RootObject obj, JavaScriptSerializer serializer)
    {
        object itemCost;
        if (customDictionary.TryGetValue(ValuesName, out itemCost) && itemCost != null)
            obj.Values = serializer.FromSingleOrArray<float>(itemCost).ToArray();
    }

    protected override void SerializeCustomProperties(RootObject obj, Dictionary<string, object> dict, JavaScriptSerializer serializer)
    {
        obj.Values.ToSingleOrArray(dict, ValuesName);
    }
}

public abstract class CustomPropertiesConverter<T> : JavaScriptConverter
{
    protected abstract IEnumerable<string> CustomProperties { get; }

    protected abstract void DeserializeCustomProperties(Dictionary<string, object> customDictionary, T obj, JavaScriptSerializer serializer);

    protected abstract void SerializeCustomProperties(T obj, Dictionary<string, object> dict, JavaScriptSerializer serializer);

    public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)
    {
        // Detach custom properties
        var customDictionary = new Dictionary<string, object>();
        foreach (var key in CustomProperties)
        {
            object value;
            if (dictionary.TryRemoveInvariant(key, out value))
                customDictionary.Add(key, value);
        }

        // Deserialize and populate all members other than "values"
        var obj = new JavaScriptSerializer().ConvertToType<T>(dictionary);

        // Populate custom properties
        DeserializeCustomProperties(customDictionary, obj, serializer);

        return obj;
    }

    public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
    {
        // Generate a default serialization.  Is there an easier way to do this?
        var defaultSerializer = new JavaScriptSerializer();
        var dict = defaultSerializer.Deserialize<Dictionary<string, object>>(defaultSerializer.Serialize(obj));

        // Remove default serializations of custom properties, if present
        foreach (var key in CustomProperties)
        {
            dict.RemoveInvariant(key);
        }

        // Add custom properties
        SerializeCustomProperties((T)obj, dict, serializer);

        return dict;
    }

    public override IEnumerable<Type> SupportedTypes
    {
        get { return new[] { typeof(T) }; }
    }
}

public static class JavaScriptSerializerObjectExtensions
{
    public static void ReplaceInvariant<T>(this IDictionary<string, T> dictionary, string key, T value)
    {
        RemoveInvariant(dictionary, key);
        dictionary.Add(key, value);
    }

    public static bool TryRemoveInvariant<T>(this IDictionary<string, T> dictionary, string key, out T value)
    {
        if (dictionary == null)
            throw new ArgumentNullException();
        var keys = dictionary.Keys.Where(k => string.Equals(k, key, StringComparison.OrdinalIgnoreCase)).ToArray();
        if (keys.Length == 0)
        {
            value = default(T);
            return false;
        }
        else if (keys.Length == 1)
        {
            value = dictionary[keys[0]];
            dictionary.Remove(keys[0]);
            return true;
        }
        else
        {
            throw new ArgumentException(string.Format("Duplicate keys found: {0}", String.Join(",", keys)));
        }
    }

    public static void RemoveInvariant<T>(this IDictionary<string, T> dictionary, string key)
    {
        if (dictionary == null)
            throw new ArgumentNullException();
        foreach (var actualKey in dictionary.Keys.Where(k => string.Equals(k, key, StringComparison.OrdinalIgnoreCase)).ToArray())
            dictionary.Remove(actualKey);
    }

    public static void ToSingleOrArray<T>(this ICollection<T> list, IDictionary<string, object> dictionary, string key)
    {
        if (dictionary == null)
            throw new ArgumentNullException();
        if (list == null || list.Count == 0)
            dictionary.RemoveInvariant(key);
        else if (list.Count == 1)
            dictionary.ReplaceInvariant(key, list.First());
        else
            dictionary.ReplaceInvariant(key, list.ToArray());
    }

    public static List<T> FromSingleOrArray<T>(this JavaScriptSerializer serializer, object value)
    {
        if (value == null)
            return null;
        if (value.IsJsonArray())
        {
            return value.AsJsonArray().Select(i => serializer.ConvertToType<T>(i)).ToList();
        }
        else
        {
            return new List<T> { serializer.ConvertToType<T>(value) };
        }
    }

    public static bool IsJsonArray(this object obj)
    {
        if (obj is string || obj is IDictionary)
            return false;
        return obj is IEnumerable;
    }

    public static IEnumerable<object> AsJsonArray(this object obj)
    {
        return (obj as IEnumerable).Cast<object>();
    }
}

Then use it like:

var serializer = new JavaScriptSerializer();
serializer.RegisterConverters(new[] { new RootObjectConverter() });
var root = serializer.Deserialize<RootObject>(json);