0

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 with a JsonConverter. I need to achieve the deserialization using only System.Web.Script.Serialization.JavaScriptSerializer in c#. Can anyone help, pls?

dbc
  • 104,963
  • 20
  • 228
  • 340
  • do you mean `json converter` = `Newtonsoft Json library` ? – Rajshekar Reddy Jun 08 '17 at 11:30
  • @RajshekarReddy Probably meant `System.Web.Script.Serialization.JavaScriptSerializer` – cbr Jun 08 '17 at 11:32
  • Yes, in every answer to this issue Newtonsoft json library is used. I cant do that due to some restrictions. I need to use System.Web.Script.Serialization.JavaScriptSerializer to deserialize the json – Sangathamilan Ravichandran Jun 08 '17 at 11:34
  • javascript deserializes json as is. There're no problems with different types of fields because javascript is dynamicaly typed language. What problem exactly do you have? – pkuderov Jun 08 '17 at 11:52
  • "System.InvalidOperationException" in System.Web.Extensions.dll Type 'System.Single' is not supported for deserialization of an array. This is the exception I am getting. I have a class which has the values defined with getters and setters. The data type of the variable is currently set to float. Deserialization works fine when the field has float value. But when the field has array value i receive the above said exception – Sangathamilan Ravichandran Jun 08 '17 at 12:00
  • @Sanga omg, then pls edit your question because `But I would want to achieve the deserialization using java script serializer.` means you're trying to achieve it in javascript, not by `System.Web.Script.Serialization.JavaScriptSerializer` in c#. – pkuderov Jun 08 '17 at 12:45
  • @Sanga you can try changing the data type of that particular field to `object`. That should solve the issue.. – Rajshekar Reddy Jun 08 '17 at 12:48
  • @RajshekarReddy That can be done, but then too either array or float can be parsed while deserialization. I would need a converter kind of method which checks what is the current data type of the field and acts according to it. Any ideas on how to achieve this? – Sangathamilan Ravichandran Jun 08 '17 at 12:55
  • @Sanga You could try to manually parse the json to check if the problematic field is an array or not (maybe using LINQ?), and deserialize the object accordingly ? – Alex Ferretti Jun 08 '17 at 13:01
  • @Sanga to check the type of your `object` variable you see this thread https://stackoverflow.com/a/1110405/2592042 – Rajshekar Reddy Jun 08 '17 at 14:38
  • There's a related answer here: https://stackoverflow.com/a/34212806/3744182 – dbc Jun 08 '17 at 19:28

1 Answers1

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);
dbc
  • 104,963
  • 20
  • 228
  • 340
  • Hi This answer works perfectly except for an error regarding the variable. It works fine for one dimensional array. How to deal when i have two dimensional array? currently it throws Exception while parsing. – Sangathamilan Ravichandran Jun 12 '17 at 06:55
  • @Sanga - it's not designed to handle that situation; it's designed to handle the situation in the [linked question](https://stackoverflow.com/questions/27131664/dealing-with-json-field-that-holds-different-types-in-c-sharp) where a property value could be a single item or an array of items. If that is not your situation, you might want to ask another question with a [mcve] that includes your actual JSON so that the community doesn't spend time solving problems that you don't have. – dbc Jun 12 '17 at 07:13
  • Hi dbc..Thanks for the Info. And I had the above problem and your answer helped me. The next json given to me had multidimensional arrays. That is why wanted to know whether minimal changes can be done to this code to handle multidimensional array.. Will post a new question for the same. Have a nice day. – Sangathamilan Ravichandran Jun 12 '17 at 07:17