5

Suppose I have a class like this:

public class Example {
    public int TypedProperty { get; set; }
    public object UntypedProperty { get; set; }
}

And suppose someone comes along and writes:

var example = new Example
{
    TypedProperty = 5,
    UntypedProperty = Guid.NewGuid()
}

If I serialize this with JsonConvert.SerializeObject(example), I get

{
  "TypedProperty": 5,
  "UntypedProperty": "24bd733f-2ade-4374-9db6-3c9f3d97b12c"
}

Ideally, I'd like to get something like this:

{
  "TypedProperty": 5,
  "UntypedProperty":
    {
      "$type": "System.Guid,mscorlib",
      "$value": "24bd733f-2ade-4374-9db6-3c9f3d97b12c"
    }
 }

But TypeNameHandling doesn't work in this scenario. How can I (de)serialize an untyped property?

5
  • If you know that you gonna serialize it, why not save it as a string? Commented Aug 4, 2016 at 21:29
  • 1
    The class isn't mine to change, unfortunately. Commented Aug 4, 2016 at 21:30
  • I do know that the actual type of the object will be something serializable by Json.net. It's only ever one of a few things. Commented Aug 4, 2016 at 21:31
  • Maybe this help's: blog.maskalik.com/asp-net/… Commented Aug 4, 2016 at 21:36
  • Maybe use UntypedToTypedValueConverter from Deserialize Dictionary<string, object> with enum values in C# Commented Aug 5, 2016 at 0:21

2 Answers 2

3

If you serialize your class with TypeNameHandling.All or TypeNameHandling.Auto, then when the UntypedProperty property would be serialized as a JSON container (either an object or array) Json.NET should correctly serialize and deserialize it by storing type information in the JSON file in a "$type" property. However, in cases where UntypedProperty is serialized as a JSON primitive (a string, number, or Boolean) this doesn't work because, as you have noted, a JSON primitive has no opportunity to include a "$type" property.

The solution is, when serializing a type with a property of type object, to serialize wrappers classes for primitive values that can encapsulate the type information, along the lines of this answer. Here is a custom JSON converter that injects such a wrapper:

public class UntypedToTypedValueConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        throw new NotImplementedException("This converter should only be applied directly via ItemConverterType, not added to JsonSerializer.Converters");
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var value = serializer.Deserialize(reader, objectType);
        if (value is TypeWrapper)
        {
            return ((TypeWrapper)value).ObjectValue;
        }
        return value;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (serializer.TypeNameHandling == TypeNameHandling.None)
        {
            Console.WriteLine("ObjectItemConverter used when serializer.TypeNameHandling == TypeNameHandling.None");
            serializer.Serialize(writer, value);
        }
        // Handle a couple of simple primitive cases where a type wrapper is not needed
        else if (value is string)
        {
            writer.WriteValue((string)value);
        }
        else if (value is bool)
        {
            writer.WriteValue((bool)value);
        }
        else
        {
            var contract = serializer.ContractResolver.ResolveContract(value.GetType());
            if (contract is JsonPrimitiveContract)
            {
                var wrapper = TypeWrapper.CreateWrapper(value);
                serializer.Serialize(writer, wrapper, typeof(object));
            }
            else
            {
                serializer.Serialize(writer, value);
            }
        }
    }
}

abstract class TypeWrapper
{
    protected TypeWrapper() { }

    [JsonIgnore]
    public abstract object ObjectValue { get; }

    public static TypeWrapper CreateWrapper<T>(T value)
    {
        if (value == null)
            return new TypeWrapper<T>();
        var type = value.GetType();
        if (type == typeof(T))
            return new TypeWrapper<T>(value);
        // Return actual type of subclass
        return (TypeWrapper)Activator.CreateInstance(typeof(TypeWrapper<>).MakeGenericType(type), value);
    }
}

sealed class TypeWrapper<T> : TypeWrapper
{
    public TypeWrapper() : base() { }

    public TypeWrapper(T value)
        : base()
    {
        this.Value = value;
    }

    public override object ObjectValue { get { return Value; } }

    public T Value { get; set; }
}

Then apply it to your type using [JsonConverter(typeof(UntypedToTypedValueConverter))]:

public class Example
{
    public int TypedProperty { get; set; }
    [JsonConverter(typeof(UntypedToTypedValueConverter))]
    public object UntypedProperty { get; set; }
}

If you cannot modify the Example class in any way to add this attribute (your comment The class isn't mine to change suggests as much) you could inject the converter with a custom contract resolver:

public class UntypedToTypedPropertyContractResolver : DefaultContractResolver
{
    readonly UntypedToTypedValueConverter converter = new UntypedToTypedValueConverter();

    // As of 7.0.1, Json.NET suggests using a static instance for "stateless" contract resolvers, for performance reasons.
    // http://www.newtonsoft.com/json/help/html/ContractResolver.htm
    // http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_DefaultContractResolver__ctor_1.htm
    // "Use the parameterless constructor and cache instances of the contract resolver within your application for optimal performance."
    // See also https://stackoverflow.com/questions/33557737/does-json-net-cache-types-serialization-information
    static UntypedToTypedPropertyContractResolver instance;

    // Explicit static constructor to tell C# compiler not to mark type as beforefieldinit
    static UntypedToTypedPropertyContractResolver() { instance = new UntypedToTypedPropertyContractResolver(); }

    public static UntypedToTypedPropertyContractResolver Instance { get { return instance; } }

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        var contract = base.CreateObjectContract(objectType);

        foreach (var property in contract.Properties.Concat(contract.CreatorParameters))
        {
            if (property.PropertyType == typeof(object)
                && property.Converter == null)
            {
                property.Converter = property.MemberConverter = converter;
            }
        }
        return contract;
    }
}

And use it as follows:

var settings = new JsonSerializerSettings 
{
    TypeNameHandling = TypeNameHandling.Auto,
    ContractResolver = UntypedToTypedPropertyContractResolver.Instance,
};

var json = JsonConvert.SerializeObject(example, Formatting.Indented, settings);

var example2 = JsonConvert.DeserializeObject<Example>(json, settings);

In both cases the JSON created looks like:

{
  "TypedProperty": 5,
  "UntypedProperty": {
    "$type": "Question38777588.TypeWrapper`1[[System.Guid, mscorlib]], Tile",
    "Value": "e2983c59-5ec4-41cc-b3fe-34d9d0a97f22"
  }
}
Sign up to request clarification or add additional context in comments.

Comments

0

Lookup SerializeWithJsonConverters.htm and ReadingWritingJSON. Call: JsonConvert.SerializeObject(example, new ObjectConverter());

class ObjectConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
  return objectType == typeof(Example);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
  throw new NotImplementedException();
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
  Example e = (Example)value;

  writer.WriteStartObject();

  writer.WritePropertyName("TypedProperty");
  writer.WriteValue(e.TypedProperty);

  writer.WritePropertyName("UntypedProperty");
  writer.WriteStartObject();

  writer.WritePropertyName("$type");
  writer.WriteValue(e.UntypedProperty.GetType().FullName);

  writer.WritePropertyName("$value");
  writer.WriteValue(e.UntypedProperty.ToString());

  writer.WriteEndObject();

  writer.WriteEndObject();
}
}

3 Comments

Thanks, but I've seen these docs. This doesn't solve the problem of deserialization.
Obviously. ReadJson is not implemented, though very simple. Question was about serialization, not deserialization.
Last sentence of my question: "How can I (de)serialize an untyped property?"

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.