Search code examples
c#json.nettypeconverter

Can I use TypeConverter attribute on single property of a model for Json serializer?


public class JsonModel
{
    [TypeConverter(typeof(CidNumberConvertor))]
    [JsonProperty("cid_number")]
    public Cid CidNumber;

    [TypeConverter(typeof(CidHexaConvertor))]
    [JsonProperty("cid_hexa")]
    public Cid CidHexa;

    [JsonProperty("cid_default")]
    public Cid CidDefault;
}

Imagine I've 3 fields and all are of type Cid. I've globally registered TypeConvertor CidHexaConvertor. It seems TypeConvertor attribute is ignored on attributes itself and is invoked only when define on the class/model itself. CidHexaConvertor has method to convert string to Cid and Cid to string. I can share more code later, but it seems attributes like this are not possible. Any clue?


Solution

  • Checking for [TypeConverter(typeof(...))] attributes applied to members is not implemented out of the box in Json.NET. You could, however, create a custom JsonConverter that wraps an arbitrary TypeConverter, then apply that to your model using JsonConverterAttribute.

    First, define the following JsonConverter:

    public class TypeConverterJsonConverter : JsonConverter
    {
        readonly TypeConverter converter;
    
        public TypeConverterJsonConverter(Type typeConverterType) : this((TypeConverter)Activator.CreateInstance(typeConverterType)) { }
    
        public TypeConverterJsonConverter(TypeConverter converter)
        {
            if (converter == null)
                throw new ArgumentNullException();
            this.converter = converter;
        }
    
        public override bool CanConvert(Type objectType)
        {
            return converter.CanConvertFrom(typeof(string)) && converter.CanConvertTo(objectType);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var tokenType = reader.SkipComments().TokenType;
            if (tokenType == JsonToken.Null)
                return null;
            if (!tokenType.IsPrimitive())
                throw new JsonSerializationException(string.Format("Token {0} is not primitive.", tokenType));
            var s = (string)JToken.Load(reader);
            return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s);
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var s = converter.ConvertToInvariantString(value);
            writer.WriteValue(s);
        }
    }
    
    public static partial class JsonExtensions
    {
        public static JsonReader SkipComments(this JsonReader reader)
        {
            while (reader.TokenType == JsonToken.Comment && reader.Read())
                ;
            return reader;
        }
    
        public static bool IsPrimitive(this JsonToken tokenType)
        {
            switch (tokenType)
            {
                case JsonToken.Integer:
                case JsonToken.Float:
                case JsonToken.String:
                case JsonToken.Boolean:
                case JsonToken.Undefined:
                case JsonToken.Null:
                case JsonToken.Date:
                case JsonToken.Bytes:
                    return true;
                default:
                    return false;
            }
        }
    }
    

    Then apply it to your model as follows:

    public class JsonModel
    {
        [JsonConverter(typeof(TypeConverterJsonConverter), typeof(CidNumberConvertor))]
        [TypeConverter(typeof(CidNumberConvertor))]
        [JsonProperty("cid_number")]
        public Cid CidNumber;
    
        [JsonConverter(typeof(TypeConverterJsonConverter), typeof(CidHexaConvertor))]
        [TypeConverter(typeof(CidHexaConvertor))]
        [JsonProperty("cid_hexa")]
        public Cid CidHexa;
    
        [JsonProperty("cid_default")]
        public Cid CidDefault;
    }
    

    Notes:

    • Applying a JsonConverter overrides use of the global default TypeConverter for Cid.

    • The JsonConverterAttribute(Type,Object[]) constructor is used to pass the specific TypeConverter type to the constructor of TypeConverterJsonConverter as an argument.

    • In production code, I assume those are properties not fields.

    Sample fiddle #1 here. (In the absence of a mcve I had to create a stub implementation of Cid.)

    Alternatively, if you have many properties for which you want to use an applied TypeConverter when serializing to JSON, you can create a custom ContractResolver that instantiates and applies TypeConverterJsonConverter automatically:

    public class PropertyTypeConverterContractResolver : DefaultContractResolver
    {
        protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
        {
            var property = base.CreateProperty(member, memberSerialization);
    
            if (property.Converter == null)
            {
                // Can more than one TypeConverterAttribute be applied to a given member?  If so,
                // what should we do?
                var attr = property.AttributeProvider.GetAttributes(typeof(TypeConverterAttribute), false)
                    .OfType<TypeConverterAttribute>()
                    .SingleOrDefault();
                if (attr != null)
                {
                    var typeConverterType = GetTypeFromName(attr.ConverterTypeName, member.DeclaringType.Assembly);
                    if (typeConverterType != null)
                    {
                        var jsonConverter = new TypeConverterJsonConverter(typeConverterType);
                        if (jsonConverter.CanConvert(property.PropertyType))
                        {
                            property.Converter = jsonConverter;
                            // MemberConverter is obsolete or removed in later versions of Json.NET but
                            // MUST be set identically to Converter in earlier versions.
                            property.MemberConverter = jsonConverter;
                        }
                    }
                }
            }
    
            return property;
        }
    
        static Type GetTypeFromName(string typeName, Assembly declaringAssembly)
        {
            // Adapted from https://referencesource.microsoft.com/#System/compmod/system/componentmodel/PropertyDescriptor.cs,1c1ca94869d17fff
            if (string.IsNullOrEmpty(typeName))
            {
                return null;
            }
    
            Type typeFromGetType = Type.GetType(typeName);
    
            Type typeFromComponent = null;
            if (declaringAssembly != null)
            {
                if ((typeFromGetType == null) ||
                    (declaringAssembly.FullName.Equals(typeFromGetType.Assembly.FullName)))
                {
                    int comma = typeName.IndexOf(',');
                    if (comma != -1)
                        typeName = typeName.Substring(0, comma);
                    typeFromComponent = declaringAssembly.GetType(typeName);
                }
            }
    
            return typeFromComponent ?? typeFromGetType;
        }
    }
    

    Then use it as follows:

    // Cache statically for best performance.
    var resolver = new PropertyTypeConverterContractResolver();
    var settings = new JsonSerializerSettings
    {
        ContractResolver = resolver,
    };
    
    var json = JsonConvert.SerializeObject(root, Formatting.Indented, settings);
    
    var root2 = JsonConvert.DeserializeObject<JsonModel>(json, settings);
    

    Notes:

    Sample fiddle #2 here.