Anshul
Anshul

Reputation: 1382

Serializing/deserializing System.Object using a different type

I would like to know how to implement a custom serializer/deserializer for the following class:

[JsonConverter(typeof(UnderlyingTypeConverter))]
public class ZoneProgramInput
{
    public string Name { get; set; }
    public Subject<object> InputSubject { get; }
    private IDisposable InputDisposable { get; set; }
    public Type Type { get; set; }
    public object Value { get; set; }
}

The requirement is that I would like to serialize/deserialize the property Value (of type object) with the type stored in the property Type and not with the type object. So if I have the following code:

var zpi = new ZoneProgramInput() { Type = typeof(System.Drawing.Color), Value = System.Drawing.Color.Red };
var serializedZpi = JsonConvert.SerializeObject(zpi);
var deserializedZpi = JsonConvert.DeserializeObject<ZoneProgramInput>(serializedZpi);

The variable deserializedZpi contains a deserialized instance of zpi, and the deserialized.Value should be of type System.Drawing.Color. Without a custom converter, it deserializes as a string rather than a System.Drawing.Color. As a note, I just chose System.Drawing.Color arbitrarily. This type can be anything.

I have a converter class called UnderlyingTypeConverter (which is set as the converter for ZoneProgramInput in the above code):

public class UnderlyingTypeConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {

    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {

    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(ZoneProgramInput);
    }
}

What should I fill into the ReadJson/WriteJson method to make sure that the Value property serializes and deserializes with the type stored in the Type property? I've tried looking around Google and StackOverflow for examples for ReadJson/WriteJson, but I haven't found anything that can help me find the type in this manner. Thank you for your help in advance.

PS: I know I could possibly use generics, but I already tried that. Making ZoneProgramInput take a generic type parameter and making Value of that type still serializes/deserializes Value as a string. I also tried using C# dynamic keyword and it's the same result. Also TypeNameHandling apparently doesn't work with things that are defined as object types. It just serializes them as strings instead of objects.

Upvotes: 3

Views: 5212

Answers (2)

Stridemann
Stridemann

Reputation: 51

I'm using this way for serializing System.Object that can be a different type (it can be modified to support colections of objects).

NOTE if all objects that could be in that System.Object have some base class/interface- use it instead of object and enable this option: https://www.newtonsoft.com/json/help/html/SerializeTypeNameHandling.htm

Here is for System.Object case:

public class SerializeMe
{
    [JsonConverter(typeof(ObjectJsonConverter))]
    public JsoSerializableObjectContainer VisualData { get; set; } = new JsoSerializableObjectContainer();
}

public class JsoSerializableObjectContainer
{
    public string ObjectTypeAssemblyName { get; set; }
    public string ObjectTypeName { get; set; }
    public string ObjectTypeData { get; set; }
    [JsonIgnore]
    public object Data { get; set; }
}

public class ObjectJsonConverter : JsonConverter
{
    private static Assembly[] _assemblies;

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var dataObj = (JsoSerializableObjectContainer) value;

        if (dataObj?.Data == null)
            return;

        var objType = dataObj.Data.GetType();
        dataObj.ObjectTypeName = objType.FullName;
        dataObj.ObjectTypeAssemblyName = objType.Assembly.FullName;
        dataObj.ObjectTypeData = JsonConvert.SerializeObject(dataObj.Data);

        var ser = JsonSerializer.Create();
        ser.Serialize(writer, dataObj);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var jsonSerializer = JsonSerializer.Create();
        var container = jsonSerializer.Deserialize<JsoSerializableObjectContainer>(reader);

        if (container != null)
        {
            if (_assemblies == null)
            {
                _assemblies = AppDomain.CurrentDomain.GetAssemblies();
            }

            var assembly = _assemblies.Single(t => t.FullName == container.ObjectTypeAssemblyName);
            var deserializationType = assembly.GetType(container.ObjectTypeName);

            if (deserializationType == null)
            {
                throw new JsonException(
                    $"Can't find type for object deserialization {container.ObjectTypeName}"); //Probably type was deleted from code
            }

            var myObject = JsonConvert.DeserializeObject(container.ObjectTypeData, deserializationType);
            container.Data = myObject;
            return container;
        }

        return null;
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(JsoSerializableObjectContainer);
    }
}

Test code:

        var ser = new SerializeMe();
        ser.VisualData.Data = (int)1;
        var serializationResult = JsonConvert.SerializeObject(ser);
        var deserializedObject = JsonConvert.DeserializeObject<SerializeMe>(serializationResult);


        ser.VisualData.Data = new System.Drawing.Rectangle(0, 1, 2, 3);
        serializationResult = JsonConvert.SerializeObject(ser);
        deserializedObject = JsonConvert.DeserializeObject<SerializeMe>(serializationResult);

Note: Instead of ObjectTypeAssemblyName and ObjectTypeName you can use only second one from objType.AssemblyQualifiedName, but it will probably fail because it will also check the assembly version while searching a type. And after updating packages or dll version changes it will not able to find the type.

Upvotes: 0

Andrew Whitaker
Andrew Whitaker

Reputation: 126042

It's kind of a shame that you need to do this, but I don't really see a way around it. Here's a converter that should work:

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

    public override object ReadJson(
        JsonReader reader,
        Type objectType,
        object existingValue,
        JsonSerializer serializer)
    {
        var result = new ZoneProgramInput();

        // Deserialize into a temporary JObject
        JObject obj = serializer.Deserialize<JObject>(reader);

        // Populate the ZoneProgramInput object with the contents
        serializer.Populate(obj.CreateReader(), result);

        // Overwrite the "Value" property with the correct value based on the 
        // "Type" property.
        result.Value = 
            obj.GetValue("value", StringComparison.OrdinalIgnoreCase)
               .ToObject(result.Type, serializer);

        return result;
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(ZoneProgramInput);
    }

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

Example: https://dotnetfiddle.net/Zv57R8

Upvotes: 2

Related Questions