1
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?

dbc
  • 104,963
  • 20
  • 228
  • 340
Marek Nos
  • 65
  • 6
  • 1) I assume that should be `[JsonProperty("cid_default")]` not `[TypeConverter("cid_default")]`. 2) It would be helpful for testing purposes to have a full [mcve]. – dbc Sep 12 '18 at 21:21

1 Answers1

2

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Thanks for this extensive reply, very appreciate. I had similar solution in mind also. My idea was to basically extend converters(2) to inherit from JsonConverter also, thus implementing ReadJson/WriteJson in a way to call ConvertFrom/ConvertTo internally, but you managed to do it in much more abstract way. Good job! – Marek Nos Sep 17 '18 at 09:47