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.