Search code examples
c#jsonjavascriptserializer

JSON value is sometimes a string and sometimes an object


I have some JSON that can come in two different formats. Sometimes the location value is a string, and sometimes it is an object. This is a sample of the first format:

{
  "result": [
    {
      "upon_approval": "Proceed to Next Task",
      "location": "",
      "expected_start": ""
    }
  ]
}

Class definitions for this:

public class Result
{
    public string upon_approval { get; set; }
    public string location { get; set; }
    public string expected_start { get; set; }
}

public class RootObject
{
    public List<Result> result { get; set; }
}

Here is the JSON in the second format:

{
  "result": [
    {
      "upon_approval": "Proceed to Next Task",
      "location": {
        "display_value": "Corp-HQR",
        "link": "https://satellite.service-now.com/api/now/table/cmn_location/4a2cf91b13f2de00322dd4a76144b090"
      },
      "expected_start": ""
    }
  ]
}

Class definitions for this:

public class Location
{
    public string display_value { get; set; }
    public string link { get; set; }
}

public class Result
{
    public string upon_approval { get; set; }
    public Location location { get; set; }
    public string expected_start { get; set; }
}

public class RootObject
{
    public List<Result> result { get; set; }
}

When deserializing, I get errors when the JSON format does not match my classes, but I don't know ahead of time which classes to use because the JSON format changes. So how can I dynamically get these two JSON formats to deserialize into one set of classes?

This is how I am deserializing now:

JavaScriptSerializer ser = new JavaScriptSerializer();
ser.MaxJsonLength = 2147483647;
RootObject ro = ser.Deserialize<RootObject>(responseValue);

Solution

  • To solve this problem you'll need to make a custom JavaScriptConverter class and register it with the serializer. The serializer will load the result data into a Dictionary<string, object>, then hand off to the converter, where you can inspect the contents and convert it into a usable object. In short, this will allow you to use your second set of classes for both JSON formats.

    Here is the code for the converter:

    class ResultConverter : JavaScriptConverter
    {
        public override IEnumerable<Type> SupportedTypes
        {
            get { return new List<Type> { typeof(Result) }; }
        }
    
        public override object Deserialize(IDictionary<string, object> dict, Type type, JavaScriptSerializer serializer)
        {
            Result result = new Result();
            result.upon_approval = GetValue<string>(dict, "upon_approval");
            var locDict = GetValue<IDictionary<string, object>>(dict, "location");
            if (locDict != null)
            {
                Location loc = new Location();
                loc.display_value = GetValue<string>(locDict, "display_value");
                loc.link = GetValue<string>(locDict, "link");
                result.location = loc;
            }
            result.expected_start = GetValue<string>(dict, "expected_start");
            return result;
        }
    
        private T GetValue<T>(IDictionary<string, object> dict, string key)
        {
            object value = null;
            dict.TryGetValue(key, out value);
            return value != null && typeof(T).IsAssignableFrom(value.GetType()) ? (T)value : default(T);
        }
    
        public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    

    Then use it like this:

    var ser = new JavaScriptSerializer();
    ser.MaxJsonLength = 2147483647;
    ser.RegisterConverters(new List<JavaScriptConverter> { new ResultConverter() });
    RootObject ro = serializer.Deserialize<RootObject>(responseValue);
    

    Here is a short demo:

    class Program
    {
        static void Main(string[] args)
        {
            string json = @"
            {
              ""result"": [
                {
                  ""upon_approval"": ""Proceed to Next Task"",
                  ""location"": {
                    ""display_value"": ""Corp-HQR"",
                    ""link"": ""https://satellite.service-now.com/api/now/table/cmn_location/4a2cf91b13f2de00322dd4a76144b090""
                  },
                  ""expected_start"": """"
                }
              ]
            }";
    
            DeserializeAndDump(json);
            Console.WriteLine(new string('-', 40));
    
            json = @"
            {
              ""result"": [
                {
                  ""upon_approval"": ""Proceed to Next Task"",
                  ""location"": """",
                  ""expected_start"": """"
                }
              ]
            }";
    
            DeserializeAndDump(json);
    
        }
    
        private static void DeserializeAndDump(string json)
        {
            var serializer = new JavaScriptSerializer();
            serializer.RegisterConverters(new List<JavaScriptConverter> { new ResultConverter() });
            RootObject obj = serializer.Deserialize<RootObject>(json);
    
            foreach (var result in obj.result)
            {
                Console.WriteLine("upon_approval: " + result.upon_approval);
                if (result.location != null)
                {
                    Console.WriteLine("location display_value: " + result.location.display_value);
                    Console.WriteLine("location link: " + result.location.link);
                }
                else
                    Console.WriteLine("(no location)");
            }
        }
    }
    
    public class RootObject
    {
        public List<Result> result { get; set; }
    }
    
    public class Result
    {
        public string upon_approval { get; set; }
        public Location location { get; set; }
        public string expected_start { get; set; }
    }
    
    public class Location
    {
        public string display_value { get; set; }
        public string link { get; set; }
    }
    

    Output:

    upon_approval: Proceed to Next Task
    location display_value: Corp-HQR
    location link: https://satellite.service-now.com/api/now/table/cmn_location/4a2cf91b13f2de00322dd4a76144b090
    ----------------------------------------
    upon_approval: Proceed to Next Task
    (no location)