4

I know there are many posts about this type of situation, but all the answers I've looked up do not work for me, and I think my situation is a little different.

I have a class known as PropertyValue, and it's a meta data class that describes the property value, and then also has an attribute that contains the actual value:

public sealed class PropertyValue
{
    public PropertyValue()
    {

    }        

    public string PropertyName { get; set; }

    public string CategoryName { get; }

    public string DisplayName { get; }

    public int PropertyId { get; }

    public string TypeName { get; set;}        

    public string ToolTip { get; set;}

    public string Description { get; }        

    public object CurrentValue { get; set; }

}

The TypeName property actually says what type of object CurrentValue should be, and the values range from System.Int32 to Proprietary objects that our company has built. The problem is when I try to use JsonConvert.DeserializeObject(property) it deserializes everything except the CurrentValue property. I tried using a switch statement in the constructor for all the types we handle and creating a new instance of that class, but it's not resolving the nested values in the JSON.

Any ideas?

Edit: I'm including my JSON that shows one of our timezone classes:

{
   "PropertyName":"TimeZone",
   "CategoryName":"TBD",
   "DisplayName":"TimeZone",
   "PropertyId":15,
   "TypeName":"Namespace.TimeZoneReference",
   "ToolTip":"",
   "Description":"",
   "CurrentValue":{
      "timeZoneID":21,
      "timeZoneName":"Eastern Standard Time"
   }
}
Michael Sheely
  • 961
  • 2
  • 10
  • 31

2 Answers2

3

It sounds like you're trying to re-invent Json.NET's TypeNameHandling setting. Since you're not using this setting but are instead serializing the type name for CurrentValue yourself, you'll need to create a custom JsonConverter to populate your PropertyValue and deserialize CurrentValue to the desired type. Without that, Json.NET will deserialize the current value to either a primitive (such as long or string) or a LINQ to JSON object such as JObject for a non-primitive JSON value. (The latter is the string value with curly brackets wrapped around the two or three key/value pairs that you mention seeing in comments.)

Here is one possible converter applied to your type:

[JsonConverter(typeof(PropertyValueConverter))]
public sealed class PropertyValue
{
    public PropertyValue(object CurrentValue)
    {
        SetCurrentValue(CurrentValue);
    }

    public PropertyValue()
    {
    }

    public string PropertyName { get; set; }

    public string CategoryName { get; set; }

    public string DisplayName { get; set; }

    public int PropertyId { get; set; }

    public string TypeName { get; set; }

    public string ToolTip { get; set; }

    public string Description { get; set; }

    public object CurrentValue { get; set; }

    public void SetCurrentValue(object value)
    {
        CurrentValue = value;
        if (value == null)
            TypeName = null;
        else
            TypeName = value.GetType().AssemblyQualifiedName;
    }
}

public class PropertyValueConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(PropertyValue).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var propertyValue = (existingValue as PropertyValue ?? new PropertyValue());

        var obj = JObject.Load(reader);

        // Remove the CurrentValue property for manual deserialization, and deserialize
        var jValue = obj.GetValue("CurrentValue", StringComparison.OrdinalIgnoreCase).RemoveFromLowestPossibleParent();

        // Load the remainder of the properties
        serializer.Populate(obj.CreateReader(), propertyValue);

        // Convert the type name to a type.
        // Use the serialization binder to sanitize the input type!  See
        // https://stackoverflow.com/questions/39565954/typenamehandling-caution-in-newtonsoft-json

        if (!string.IsNullOrEmpty(propertyValue.TypeName) && jValue != null)
        {
            string typeName, assemblyName;
            ReflectionUtils.SplitFullyQualifiedTypeName(propertyValue.TypeName, out typeName, out assemblyName);

            var type = serializer.Binder.BindToType(assemblyName, typeName);
            if (type != null)
                propertyValue.SetCurrentValue(jValue.ToObject(type, serializer));
        }

        return propertyValue;
    }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

public static class JsonExtensions
{
    public static JToken RemoveFromLowestPossibleParent(this JToken node)
    {
        if (node == null)
            return null;
        var contained = node.AncestorsAndSelf().Where(t => t.Parent is JContainer && t.Parent.Type != JTokenType.Property).FirstOrDefault();
        if (contained != null)
            contained.Remove();
        // Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
        if (node.Parent is JProperty)
            ((JProperty)node.Parent).Value = null;
        return node;
    }
}

public static class ReflectionUtils
{
    // Utilities taken from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Utilities/ReflectionUtils.cs
    // I couldn't find a way to access these directly.

    public static void SplitFullyQualifiedTypeName(string fullyQualifiedTypeName, out string typeName, out string assemblyName)
    {
        int? assemblyDelimiterIndex = GetAssemblyDelimiterIndex(fullyQualifiedTypeName);

        if (assemblyDelimiterIndex != null)
        {
            typeName = fullyQualifiedTypeName.Substring(0, assemblyDelimiterIndex.GetValueOrDefault()).Trim();
            assemblyName = fullyQualifiedTypeName.Substring(assemblyDelimiterIndex.GetValueOrDefault() + 1, fullyQualifiedTypeName.Length - assemblyDelimiterIndex.GetValueOrDefault() - 1).Trim();
        }
        else
        {
            typeName = fullyQualifiedTypeName;
            assemblyName = null;
        }
    }

    private static int? GetAssemblyDelimiterIndex(string fullyQualifiedTypeName)
    {
        int scope = 0;
        for (int i = 0; i < fullyQualifiedTypeName.Length; i++)
        {
            char current = fullyQualifiedTypeName[i];
            switch (current)
            {
                case '[':
                    scope++;
                    break;
                case ']':
                    scope--;
                    break;
                case ',':
                    if (scope == 0)
                    {
                        return i;
                    }
                    break;
            }
        }

        return null;
    }
}

Sample fiddle. (I had to make several of your properties read/write since they were read-only but not set in the constructor.)

Alternatively, you could mark your CurrentValue with [JsonProperty(TypeNameHandling = TypeNameHandling.All)]:

public sealed class PropertyValue
{
    [JsonProperty(TypeNameHandling = TypeNameHandling.All)]
    public object CurrentValue { get; set; }

    // Remainder as before

Having done so, Json.NET will output a "$type" property indicating the actual type of CurrentObject, and automatically use that type during deserialization, e.g.:

  {
    "CurrentValue": {
      "$type": "Question42537050.ExampleClass1, Tile",
      "Foo": "hello"
    },
    "PropertyName": "name1",
    "CategoryName": null,
    "DisplayName": null,
    "PropertyId": 0,
    "TypeName": "Question42537050.ExampleClass1, Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
    "ToolTip": "tip1",
    "Description": null
  }

Of course, if you do this, the type name will appear twice in the JSON - once for your TypeName property, and once for Json.NET's $type property. And this setting only works for complex objects and arrays, not for primitive types.

In either case, for security reasons, you should sanitize your TypeName before creating an instance of the type, for reasons explained here. My code assumes that you have set up JsonSerializer.Binder to do this using a custom SerializationBinder, but you may instead implement some validation logic in PropertyValueConverter.ReadJson() itself.

dbc
  • 104,963
  • 20
  • 228
  • 340
0

If you are getting null CurrentValue then I would check to see if any errors are being thrown during deserialization. If CurrentValue does actually have a value (I suspect a stringified object maybe?), then You will need to write a custom JsonConverter to get it the object you want.

Mike_G
  • 16,237
  • 14
  • 70
  • 101