Json.NET - Serialize generic type wrapper without property name

前端 未结 1 1892
说谎
说谎 2020-12-07 02:06

I have a generic type that wraps a single primitive type to give it value equality semantics

public class ValueObject
{
    public T Value { get; }
         


        
相关标签:
1条回答
  • 2020-12-07 02:45

    You can do this with a custom JsonConverter similar to the ones shown in Json.Net: Serialize/Deserialize property as a value, not as an object. However, since ValueObject<T> does not have a non-generic method to get and set the Value as an object, you will need to use reflection.

    Here's one approach:

    class ValueConverter : JsonConverter
    {
        static Type GetValueType(Type objectType)
        {
            return objectType
                .BaseTypesAndSelf()
                .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ValueObject<>))
                .Select(t => t.GetGenericArguments()[0])
                .FirstOrDefault();
        }
    
        public override bool CanConvert(Type objectType)
        {
            return GetValueType(objectType) != null;
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            // You need to decide whether a null JSON token results in a null ValueObject<T> or 
            // an allocated ValueObject<T> with a null Value.
            if (reader.SkipComments().TokenType == JsonToken.Null)
                return null;
            var valueType = GetValueType(objectType);
            var value = serializer.Deserialize(reader, valueType);
    
            // Here we assume that every subclass of ValueObject<T> has a constructor with a single argument, of type T.
            return Activator.CreateInstance(objectType, value);
        }
    
        const string ValuePropertyName = nameof(ValueObject<object>.Value);
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());
            var valueProperty = contract.Properties.Where(p => p.UnderlyingName == ValuePropertyName).Single();
            // You can simplify this to .Single() if ValueObject<T> has no other properties:
            // var valueProperty = contract.Properties.Single();
            serializer.Serialize(writer, valueProperty.ValueProvider.GetValue(value));
        }
    }
    
    public static partial class JsonExtensions
    {
        public static JsonReader SkipComments(this JsonReader reader)
        {
            while (reader.TokenType == JsonToken.Comment && reader.Read())
                ;
            return reader;
        }
    }
    
    public static class TypeExtensions
    {
        public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
        {
            while (type != null)
            {
                yield return type;
                type = type.BaseType;
            }
        }
    }
    

    You could then apply the converter directly to ValueType<T> like so:

    [JsonConverter(typeof(ValueConverter))]
    public class ValueObject<T>
    {
        // Remainder unchanged
    }
    

    Or apply it in settings instead:

    var settings = new JsonSerializerSettings
    {
        Converters = { new ValueConverter() },
        ContractResolver = new CamelCasePropertyNamesContractResolver() 
    };
    var customerAsJson = JsonConvert.SerializeObject(customer, Formatting.Indented, settings);
    

    Working sample .Net fiddle #1 here.

    Alternatively, you might consider adding a non-generic method to access the value as an object, e.g. like so:

    public interface IHasValue
    {
        object GetValue(); // A method rather than a property to ensure the non-generic value is never serialized directly.
    }
    
    public class ValueObject<T> : IHasValue
    {
        public T Value { get; }
        public ValueObject(T value) => Value = value;
    
        // various other equality members etc...
    
        #region IHasValue Members
    
        object IHasValue.GetValue() => Value;
    
        #endregion
    }
    

    With this addition, WriteJson() becomes much simpler:

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            serializer.Serialize(writer, ((IHasValue)value).GetValue());
        }
    

    Working sample .Net fiddle #2 here.

    Notes:

    • ReadJson() assumes that every subclass of Value<T> has a public constructor taking a single argument of type T.

    • Applying the converter directly to ValueType<T> using [JsonConverter(typeof(ValueConverter))] will have slightly better performance, since CanConvert need never get called. See Performance Tips: JsonConverters for details.

    • You need to decide how to handle a null JSON token. Should it result in a null ValueType<T>, or an allocated ValueType<T> with a null Value?

    • In the second version of ValueType<T> I implemented IHasValue.GetValue() explicitly to discourage its use in cases where an instance of ValueType<T> is used in statically typed code.

    • If you really only want to apply the converter to types subclassing ValueObject<T> and not ValueObject<T> itself, in GetValueType(Type objectType) add a call to .Skip(1):

      static Type GetValueType(Type objectType)
      {
          return objectType
              .BaseTypesAndSelf()
              .Skip(1) // Do not apply the converter to ValueObject<T> when not subclassed
              .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ValueObject<>))
              .Select(t => t.GetGenericArguments()[0])
              .FirstOrDefault();
      }
      

      And then apply the converter in JsonSerializerSettings.Converters rather than directly to ValueObject<T>.

    0 讨论(0)
提交回复
热议问题