How can I achieve the following JSON representation of Id class when used in another class?
class Car
{
public StringId Id { get; set; }
public string Name { get; set; }
}
class StringId
{
public string Value { get; set; }
}
// ---------------------------------------------
// Desired representation
{ "Id": "someId", "Name": "Ford" }
// Default (undesired) representation
{ "Id" : { "Value": "someId" }, "Name": "Ford" }
You could add a TypeConverter for StringId. Json.NET will pick up the type converter and use it to convert it from and to a string:
[TypeConverter(typeof(StringIdConverter))]
class StringId
{
public string Value { get; set; }
}
class StringIdConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
return true;
return base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(StringId))
return true;
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
if (value is string)
{
return new StringId { Value = (string)value };
}
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string) && value is StringId)
{
return ((StringId)value).Value;
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
If your string representation contains embedded numeric or date/time data, be sure to convert that data using the culture passed in rather than the default, current culture. Json.NET will call the converter with the correct culture, which is the invariant culture by default, thus ensuring the generated JSON files are portable between cultures.
Sample fiddle.
Note however that, if you are using .Net Core, support for type converters was only added as of Json.NET 10.0.1. And support for type converters in Json.NET Portable builds is not available as of 10.0.3.
Alternatively, if you don't mind adding Json.NET-specific attributes to your type, you could use a custom JsonConverter:
[JsonConverter(typeof(StringIdConverter))]
class StringId
{
public string Value { get; set; }
}
class StringIdConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(StringId);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var token = JToken.Load(reader);
return new StringId { Value = (string)token };
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var id = (StringId)value;
writer.WriteValue(id.Value);
}
}
You can also set the converter in global settings.
Sample fiddle.
You can override the ToString method of the StringId class to return the value
public override string ToString()
{
return this.Value;
}
You will need a TypeConverter later to deserialize from string to StringId
public class StringIdConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string)
{
return new StringId(value.ToString());
}
return base.ConvertFrom(context, culture, value);
}
}
And decorate your StringId class with this attribute
[TypeConverter(typeof(StringIdConverter))]
public class StringId{
...
}
Related
I'm using DescriptionAttribute on Enums to provide a description that includes spaces. I've created some extension methods that will return the string description of an enum and/or return the enum value from a string description.
Now I want my WebAPI to use these extension methods to handle Enum Type Conversion instead of the default TypeConverter so that I can pass a value like "Car Wash" and have it mapped to an Enum.
Is there a way to override the default string to Enum TypeConverter?
Environment
.NetCore 2.x
Update - my current code
My current code does great when the Controller serializes an object to JSON to be sent to the client. Given the below Enum, an enum value of 0 will result in the client getting a string "Mental Health" -- perfect. However, now when the client sends "Mental Health" back to the server -- I need that converted back into AgencyTpes.MentalHealth. Right now, the binding engine throws an error.
//Example Enum
public enum AgencyTypes {
[Description("Mental Health")]
MentalHealth,
Corrections,
[Description("Drug & Alcohol")]
DrugAndAlcohol,
Probation
}
My Enum Extensions That Work With DescriptionAttribute
public static class EnumExtensions
{
public static string ToDisplayString(this Enum values)
{
var attribute = value.GetType().GetMember(value.ToString())
.FirstOrDefault()?.GetCustomAttribute<DescriptionAttribute>();
return attribute ?.Description ?? value.ToString();
}
public static object GetValueFromDescription(string description, Type enumType)
{
return Convert.ChangeType(LookupDescription(description,enumType),enumType);
}
public static T GetValueFromDescription<T>(string description) where T: struct
{
return (T)LookupDescription(description, typeof(T));
}
private static object LookupDescription(string description, Type enumType)
{
if(!enumType.IsEnum)
throw new ArgumentException("Type provided must be an Enum", enumType.Name);
foreach(var field in enumType.GetFields())
{
var attribute = Attribute.GetCustomAttribute(field, tyepof(DescriptionAttribute)) as DescriptionAttribute;
if((attribute != null && attribute.Description == description)
|| field.Name == description)
{
return field.GetValue(null);
}
}
throw new ArgumentException($"Requested value for '{description}' in enum {enumType.Name} was not found", nameof(description));
}
}
My JSON override to enable Controllers to convert enum to string
//Startup.cs
services.AddMvc().SetCompatibilityVersion(Compatibility.Version_2_2)
.AddJsonOptions(options =>
{
options.SerializerSettings.Converters.Add(new StringAnnotationEnumConverter());
});
public class StringAnnotationEnumConverter : StringEnumConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var token = JToken.Load(reader);
var value = token.ToString();
if(reader.TokenType == JsonToken.String)
return EnumExtensions.GetValueFromDescription(value, objectType);
else
return base.ReadJson(reader, objectType, existingValue, serializer);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if(value == null)
{
writer.WriteNull();
return;
}
Enum e = (Enum)value;
string enumName = e.ToDisplayString();
if(string.IsNullOrWhiteSpace(enumName))
throw new JsonSerializationException(String.Format("Integer value {0} is not allowed.",e.ToString("D")));
writer.WriteValue(enumName);
}
}
Update 2 - WebAPI
Here is the example code of the Controller & Domain Object
public class Agency
{
public int Id {get; set;}
public string Name {get; set;}
public AgencyTypes AgencyType {get; set;}
...
}
[ApiController]
public class AgencyController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Agency([FromForm] Agency agency)
{
...
}
}
You could try overriding the default EnumConverter to do your custom attribute check
public class MyEnumConverter: EnumConveter {
public MyEnumConverter(Type type) : base(type) {
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) {
if (value is string) {
var enumString = (string)value;
return EnumExtensions.GetValueFromDescription(enumString, EnumType);
}
return base.ConvertFrom(context, culture, value);
}
}
And decorating your enum with TypeConverter attribute
[TypeConverter(typeof(MyEnumConverter))]
public enum AgencyTypes {
[System.ComponentModel.Description("Mental Health")]
MentalHealth,
Corrections,
[System.ComponentModel.Description("Drug & Alcohol")]
DrugAndAlcohol,
Probation
}
Suppose I have a class like this:
public class Example {
public int TypedProperty { get; set; }
public object UntypedProperty { get; set; }
}
And suppose someone comes along and writes:
var example = new Example
{
TypedProperty = 5,
UntypedProperty = Guid.NewGuid()
}
If I serialize this with JsonConvert.SerializeObject(example), I get
{
"TypedProperty": 5,
"UntypedProperty": "24bd733f-2ade-4374-9db6-3c9f3d97b12c"
}
Ideally, I'd like to get something like this:
{
"TypedProperty": 5,
"UntypedProperty":
{
"$type": "System.Guid,mscorlib",
"$value": "24bd733f-2ade-4374-9db6-3c9f3d97b12c"
}
}
But TypeNameHandling doesn't work in this scenario. How can I (de)serialize an untyped property?
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"
}
}
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();
}
}
But it does. I have created a type converter that implements CanConvertFrom(), defined the type converter on type type. Everything runs fine. This is just XAML that defines design time data for Blend, but the error is very annoying.
It appears that VS is just not trying to use the type converter. Is there some place that you have to register type converters so that they will be used at design time by Visual Studio?
[TypeConverter(typeof(MyTypeTypeConverter))]
[DataContract]
public struct MyType
{
[DataMember]
internal readonly float _Value;
public MyType(float value)
{
_Value = (float)Math.Round(value, 3);
}
}
public class MyTypeTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(String))
return true;
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
if (value is string)
{
float f;
if (!Single.TryParse((string)value, out f))
return null;
return new MyType(f);
}
return base.ConvertFrom(context, culture, value);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string))
return true;
return base.CanConvertTo(context, destinationType);
}
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string))
{
return ((MyType)value).ToString();
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
I have following enumeration.
public enum Digits
{One, Two, Three}
and a property with two entries.
public List<Digits> DigitList{get;set;}
DigitList.Add(Digits.One); DigitList.Add(Digits.Three);
When this object is bound to PropertyGrid it is displayed as (Collection) and when it is opened (using small browse button) an exception with (no useful message) is displayed. I am confused how the PropertyGrid interprets list of enumerations.
I searched for a solution, but all i could find was about how to bind a enum value, not list of enums.
You have to create a TypeConverter Class that will help the PropertyEditor to parse the Enum into a PropertyEditor.
Sample TypeConverter
public class FooDataTypeConverter : TypeConverter
{
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
{
return true;
}
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
{
return base.GetStandardValues(context);
}
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
return true;
return (sourceType.Equals(typeof(Enum)));
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return (destinationType.Equals(typeof(String)));
}
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
if (value is string)
{
return (ValidationDataType)Enum.Parse(typeof(ValidationDataType), value.ToString(), true);
}
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
if (!destinationType.Equals(typeof(string)))
throw new ArgumentException("Can only convert to string", "destinationType");
if (!value.GetType().BaseType.Equals(typeof(Enum)))
throw new ArgumentException("Can only convert an instance of enum", "value");
string name = value.ToString();
object[] attr =
value.GetType().GetField(name).GetCustomAttributes(typeof(DescriptionAttribute), false);
return (attr.Length > 0) ? ((DescriptionAttribute)attr[0]).Description : name;
}
}
After add this declaration to the enums that you want to parse in a propertyeditor.
[TypeConverter(typeof(FooDataTypeConverter ))]
public enum ValidationDataType
{
/// <summary>
/// None
/// </summary>
[Description("None")]
None,
.....
}
The last step is to add it to the property of your component that will show in the propertyeditor
[Category("Behavior")]
[Description("Gets or sets the type of data that will be compared")]
[TypeConverter(typeof(DataTypeConverter))]
[EditorAttribute(typeof(ValidatorTypeEditor), typeof(System.Drawing.Design.UITypeEditor))]
public ValidationDataType Type
{
get { return this.type; }
set
{
this.type = value;
if (this is RangeValidator)
{
this.SetRange();
}
}
}
I can't convert showing property from uint to string format in PropertyGrid control. This is what I do:
var fruits = new SortedDictionary<uint, string>
{
{0, "Apple"},
{1, "Orange"},
{3, "Watermelon"},
};
public class FruitConverter : StringConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context,
Type sourceType)
{
if (sourceType == typeof(uint) && fruits.ContainsKey(sourceType))
return true;
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture,
object value)
{
if (sourceType == typeof(uint) && fruits.ContainsKey(sourceType))
return fruits[value];
return base.ConvertFrom(context, culture, value);
}
}
public class Fruit
{
[ReadOnly(true)]
[DisplayName("Type of fruit")]
[TypeConverter(typeof(FruitConverter))]
public uint FruitTypeCode { get; set; }
}
But property FruitTypeCode is still is shown as uint and not as a string, what I did wrong ?
This should work:
public class FruitConverter : TypeConverter
{
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
return fruits[(uint)value];
}
}