JSON.net (de)serialize untyped property

后端 未结 2 1687
慢半拍i
慢半拍i 2020-12-07 03:30

Suppose I have a class like this:

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


        
相关标签:
2条回答
  • 2020-12-07 03:45

    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"
      }
    }
    
    0 讨论(0)
  • 2020-12-07 03:55

    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();
    }
    }
    
    0 讨论(0)
提交回复
热议问题