Background
In .NET Core, default controller model binding fails (produces null value for your action argument) if your model contains an enum anywhere in its hierarchy and the supplied values don't match EXACTLY with the names in the enum. White-space or odd capitalization breaks the binding, and that seems unfriendly for the consumer of my API endpoint.
My solution
I created a model binder provider, which uses reflection to determine if somewhere in the target binding type there exists an enum; if this check is true, it returns a custom model binder (constructed by passing along the enum type) which uses Regex/string manipulation (gross) to scan the request body for the enum values and make an effort to resolve them to a name in that enum type, before passing JsonConvert for deserializing.
This solution is, in my opinion, far too complex and ugly for what I'm trying to achieve.
What I'd like is something like a JsonConvert attribute (for my enum fields) that makes this effort during binding/deserialization. Newtonsoft's out of the box solution (StringEnumConverter) doesn't try to adjust the string to fit the enum type (fair, I suppose), but I can't extend Newtonsoft's functionality here because it relies on a lot of internal classes (without copying and pasting a ton's of their code).
Is there a piece in the pipeline somewhere I'm missing that could be better leveraged to fit this need?
P.S. I placed this here rather than code review (it's too theoretical) or software engineering (too specific); please advise if it's not the right place.
I've used a Type Safe Enum pattern for this, I think it will work for you. With the TypeSafeEnum you can control what is mapped to the JSON using Newtonsoft's JsonConverter attribute. Since you have no code to post I've built up a sample.
Base class used by your application's TypeSafeEnums:
public abstract class TypeSafeEnumBase
{
protected readonly string Name;
protected readonly int Value;
protected TypeSafeEnumBase(int value, string name)
{
this.Name = name;
this.Value = value;
}
public override string ToString()
{
return Name;
}
}
Sample type implemented as a TypeSafeEnum, it would have normally been a plain Enum, including Parse and TryParse methods:
public sealed class BirdType : TypeSafeEnumBase
{
private const int BlueBirdId = 1;
private const int RedBirdId = 2;
private const int GreenBirdId = 3;
public static readonly BirdType BlueBird =
new BirdType(BlueBirdId, nameof(BlueBird), "Blue Bird");
public static readonly BirdType RedBird =
new BirdType(RedBirdId, nameof(RedBird), "Red Bird");
public static readonly BirdType GreenBird =
new BirdType(GreenBirdId, nameof(GreenBird), "Green Bird");
private BirdType(int value, string name, string displayName) :
base(value, name)
{
DisplayName = displayName;
}
public string DisplayName { get; }
public static BirdType Parse(int value)
{
switch (value)
{
case BlueBirdId:
return BlueBird;
case RedBirdId:
return RedBird;
case GreenBirdId:
return GreenBird;
default:
throw new ArgumentOutOfRangeException(nameof(value), $"Unable to parse for value, '{value}'. Not found.");
}
}
public static BirdType Parse(string value)
{
switch (value)
{
case "Blue Bird":
case nameof(BlueBird):
return BlueBird;
case "Red Bird":
case nameof(RedBird):
return RedBird;
case "Green Bird":
case nameof(GreenBird):
return GreenBird;
default:
throw new ArgumentOutOfRangeException(nameof(value), $"Unable to parse for value, '{value}'. Not found.");
}
}
public static bool TryParse(int value, out BirdType type)
{
try
{
type = Parse(value);
return true;
}
catch
{
type = null;
return false;
}
}
public static bool TryParse(string value, out BirdType type)
{
try
{
type = Parse(value);
return true;
}
catch
{
type = null;
return false;
}
}
}
Container to handle type safe conversion, so you don't need to create a converter for every type safe implemented, and to prevent changes in the TypeSafeEnumJsonConverter when new type safe enums are implemented:
public class TypeSafeEnumConverter
{
public static object ConvertToTypeSafeEnum(string typeName, string value)
{
switch (typeName)
{
case "BirdType":
return BirdType.Parse(value);
//case "SomeOtherType": // other type safe enums
// return // some other type safe parse call
default:
return null;
}
}
}
Implements Newtonsoft's JsonConverter which in turn calls our TypeSafeEnumConverter
public class TypeSafeEnumJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
var types = new[] { typeof(TypeSafeEnumBase) };
return types.Any(t => t == objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
string name = objectType.Name;
string value = serializer.Deserialize(reader).ToString();
return TypeSafeEnumConversion.ConvertToTypeSafeEnum(name, value); // call to our type safe converter
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null && serializer.NullValueHandling == NullValueHandling.Ignore)
{
return;
}
writer.WriteValue(value?.ToString());
}
}
Sample object that uses our BirdType and sets the converter to use:
public class BirdCoup
{
[JsonProperty("bird-a")]
[JsonConverter(typeof(TypeSafeEnumJsonConverter))] // sets the converter used for this type
public BirdType BirdA { get; set; }
[JsonProperty("bird-b")]
[JsonConverter(typeof(TypeSafeEnumJsonConverter))] // sets the converter for this type
public BirdType BirdB { get; set; }
}
Usage example:
// sample #1, converts value with spaces to BirdTyp
string sampleJson_1 = "{\"bird-a\":\"Red Bird\",\"bird-b\":\"Blue Bird\"}";
BirdCoup resultSample_1 =
JsonConvert.DeserializeObject<BirdCoup>(sampleJson_1, new JsonConverter[]{new TypeSafeEnumJsonConverter()});
// sample #2, converts value with no spaces in name to BirdType
string sampleJson_2 = "{\"bird-a\":\"RedBird\",\"bird-b\":\"BlueBird\"}";
BirdCoup resultSample_2 =
JsonConvert.DeserializeObject<BirdCoup>(sampleJson_2, new JsonConverter[] { new TypeSafeEnumJsonConverter() });
Related
Is there any built in settings in json.net to disable the conversion of numbers to booleans. I would prefer it to return an error instead : (without writing a Custom Converter for each class that has a bool property )
public class MyClass
{
public bool flag { get; set; }
}
var str = #"{"flag":123}";
MyClass result = JsonConvert.DeserializeObject<MyClass>(str) ; // flag is true !
You shouldn't use different types in one property to get one bool result.
But if you wish not to declare one custom JsonConvert for every property in the class, you can always create DefaultSettings in your code.
void Initialize()
{
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
Converters = new List<JsonConverter>
{
new BoolJsonConverter()
}
};
}
// Create JsonConvert you want to add to DefaultSettings
public class BoolJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(bool);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
}
}
var json = #"{""flag"":123}"
var myClass = JsonConvert.DeserializeObject<MyClass>(json);
I'm assuming this is a Deserialize only situation. If that is the case, you could have your int value go into an int and use another var to hold the bool. Something like -
public class MyClass
{
public int flagInt { get; set; }
[JsonIgnore]
public bool flag
{
get
{
switch (flagInt)
{
case 0: return false;
case 1: return true;
default: throw new Exception("Cannot convert flag to bool");
}
}
}
}
Note that I don't have a setter on flag so doing something like flag = false will not have expected results and will not serialize correctly either.
How can i set FloatParseHandling.Decimal for a custom JsonConverter?
we have a struct DecimalDbValue that internally only holds one decimal field that i want to be de/serialized for all its types.
It uses a magic number (decimal.MinValue) for indicating a "null" value. It was created before .net 2.0 having nullable value types!
This is a spripped down version of our struct::
[Serializable]
[JsonConverter(typeof(DecimalDbValueJsonConverter))]
public struct DecimalDbValue : ISerializable
{
private readonly Decimal _decValue;
public DecimalDbValue(
decimal init)
{
_decValue = init;
}
[JsonConstructor]
public DecimalDbValue(
decimal? init)
{
if (init.HasValue)
_decValue = init.Value;
else
_decValue = decimal.MinValue;
}
private DecimalDbValue(
SerializationInfo objSerializationInfo,
StreamingContext objStreamingContext)
{
_decValue = objSerializationInfo.GetDecimal("value");
}
public bool IsNotNull
{
get
{
return !IsNull;
}
}
public bool IsNull
{
get
{
return _decValue.Equals(Decimal.MinValue);
}
}
public Decimal Value
{
get
{
return _decValue;
}
}
public void GetObjectData(
SerializationInfo info,
StreamingContext context)
{
info.AddValue("value", _decValue);
}
}
I created a JsonConverter:
class DecimalDbValueJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(DecimalDbValue) == objectType;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var value = reader.Value == null ? (decimal?)null : Convert.ToDecimal(reader.Value);
return new DecimalDbValue(value);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var dbValue = (DecimalDbValue)value;
if (dbValue.IsNull)
writer.WriteNull();
else
writer.WriteValue(dbValue.Value);
}
}
and set attribute [JsonConverter(typeof(DecimalDbValueJsonConverter))] on the DecimalDbValue struct
I have added a test:
[Test]
public void TestMaxDecimalDbValue()
{
var s = new DecimalDbValue(decimal.MaxValue);
var json = JsonConvert.SerializeObject(s, Formatting.Indented);
var x = JsonConvert.DeserializeObject<DecimalDbValue>(json);
Assert.AreEqual(s, x);
}
but it throws:
System.OverflowException : Value was either too large or too small for a Decimal.
How can i set FloatParseHandling.Decimal for the JsonConverter? How to make it work for MaxValue as well? Is there any other way?
Actually i would like to have it serialize/deserialized exactly like an decimal? (nullable decimal)
Thanks
It seems to be impossible.
Therefor i removed the JsonConverter completely.
Set this attribute on the struct:
[JsonObject(MemberSerialization.OptIn)]
as well as this on the constructor:
[JsonConstructor]
public DecimalDbValue(
decimal? init) //IMPORTANT: if you change the name of init - rename the JsonProperty below too - you might break all existing json out there thou!
as well as this on the get property:
[JsonProperty("init")]
public Decimal Value
{
get
{
return _decValue;
}
}
Unfortunately this bloats the json:
{
"init": 79228162514264337593543950335.0
}
In case it helps, you can set it for the whole JsonSerializer:
var settings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
FloatParseHandling = FloatParseHandling.Decimal
};
_innerSerializer = Newtonsoft.Json.JsonSerializer.CreateDefault(settings);
You might also want to have a look on FloatFormatHandling to handle your nullables.
https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_FloatFormatHandling.htm
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
}
I am receiving json from a server which I am converting to an object using Json.Net. For one member I am using the StringEnumConverter which works really perfect.
However all of a sudden the server decided to use strings which also can start with a number which results in a JsonSerilizationException - obviously because enums cannot start with a number in .Net.
Now I am trying to find a solution to handle that.My first approach was to add a "_" when Reading the Json (so my enums in the code would have a starting _ when they are followed by a number) and when writing the json I would delete the starting _ (if a number is following). To achieve this I copied the StringEnumConverter into my namespace and tried to change the according part in the WriteJson and ReadJson methods. However I cannot use the StringEnumConverter since there are other dependencies I cannot access in my own namespace.
Is there any elegant solution to this problem?
You can create a JsonConverter and trim the digits from the front
public class Json_34159840
{
public static string JsonStr = #"{""enum"":""1Value"",""name"":""James"",""enum2"":""1""}";
public static void ParseJson()
{
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
Converters = new List<JsonConverter> { new EnumConverter() }
};
// Later on...
var result = JsonConvert.DeserializeObject<JsonClass>(JsonStr);
Console.WriteLine(result.Enum);
Console.WriteLine(result.Enum2);
Console.WriteLine(result.Name);
}
}
public class EnumConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var str = value.ToString();
if (Regex.IsMatch(str, #"^_"))
{
writer.WriteValue(str.Substring(1));
}
else
{
writer.WriteValue(str);
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var value = reader.Value.ToString();
if (Regex.IsMatch(value, #"^\d+$"))
{
return Enum.Parse(objectType, value);
}
if (Regex.IsMatch(value, #"^\d+"))
{
value = "_" + value;
}
return Enum.Parse(objectType, value);
}
public override bool CanConvert(Type objectType)
{
//You might want to do a more specific check like
//return objectType == typeof(JsonEnum);
return objectType.IsEnum;
}
}
public enum JsonEnum
{
_0Default,
_1Value
}
public class JsonClass
{
public string Name { get; set; }
public JsonEnum Enum { get; set; }
public JsonEnum Enum2 { get; set; }
}
Hope this helps.
EDIT: Added support for integers :D
A simple strategy is to deserialize the value into a string property and then convert into your own data type (enum or otherwise) via an accessor method or a secondary getter-only property.
I am trying to get SignalR to work with custom JsonSerializerSettings for its payload, specifically I'm trying to set TypeNameHandling = TypeNameHandling.Auto.
The problem seems to be, that SignalR uses the settings in hubConnection.JsonSerializer and GlobalHost.DependencyResolver.Resolve<JsonSerializer>() for its internal data structures as well which then causes all kinds of havoc (internal server crashes when I set TypeNameHandling.All as the most crass example, but with TypeNameHandling.Auto I also get problems, particularly when IProgress<> callbacks are involved).
Is there any workaround or am I just doing it wrong?
Sample code to demonstrate:
Server:
class Program
{
static void Main(string[] args)
{
using (WebApp.Start("http://localhost:8080"))
{
Console.ReadLine();
}
}
}
public class Startup
{
public void Configuration(IAppBuilder app)
{
var hubConfig = new HubConfiguration()
{
EnableDetailedErrors = true
};
GlobalHost.DependencyResolver.Register(typeof(JsonSerializer), ConverterSettings.GetSerializer);
app.MapSignalR(hubConfig);
}
}
public interface IFoo
{
string Val { get; set; }
}
public class Foo : IFoo
{
public string Val { get; set; }
}
public class MyHub : Hub
{
public IFoo Send()
{
return new Foo { Val = "Hello World" };
}
}
Client:
class Program
{
static void Main(string[] args)
{
Task.Run(async () => await Start()).Wait();
}
public static async Task Start()
{
var hubConnection = new HubConnection("http://localhost:8080");
hubConnection.JsonSerializer = ConverterSettings.GetSerializer();
var proxy = hubConnection.CreateHubProxy("MyHub");
await hubConnection.Start();
var result = await proxy.Invoke<IFoo>("Send");
Console.WriteLine(result.GetType());
}
Shared:
public static class ConverterSettings
{
public static JsonSerializer GetSerializer()
{
return JsonSerializer.Create(new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.All
});
}
}
This can be done by taking advantage of the fact that your types and the SignalR types are in different assemblies. The idea is to create a JsonConverter that applies to all types from your assemblies. When a type from one of your assemblies is first encountered in the object graph (possibly as the root object), the converter would temporarily set jsonSerializer.TypeNameHandling = TypeNameHandling.Auto, then proceed with the standard serialization for the type, disabling itself for the duration to prevent infinite recursion:
public class PolymorphicAssemblyRootConverter : JsonConverter
{
[ThreadStatic]
static bool disabled;
// Disables the converter in a thread-safe manner.
bool Disabled { get { return disabled; } set { disabled = value; } }
public override bool CanWrite { get { return !Disabled; } }
public override bool CanRead { get { return !Disabled; } }
readonly HashSet<Assembly> assemblies;
public PolymorphicAssemblyRootConverter(IEnumerable<Assembly> assemblies)
{
if (assemblies == null)
throw new ArgumentNullException();
this.assemblies = new HashSet<Assembly>(assemblies);
}
public override bool CanConvert(Type objectType)
{
return assemblies.Contains(objectType.Assembly);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
using (new PushValue<bool>(true, () => Disabled, val => Disabled = val)) // Prevent infinite recursion of converters
using (new PushValue<TypeNameHandling>(TypeNameHandling.Auto, () => serializer.TypeNameHandling, val => serializer.TypeNameHandling = val))
{
return serializer.Deserialize(reader, objectType);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
using (new PushValue<bool>(true, () => Disabled, val => Disabled = val)) // Prevent infinite recursion of converters
using (new PushValue<TypeNameHandling>(TypeNameHandling.Auto, () => serializer.TypeNameHandling, val => serializer.TypeNameHandling = val))
{
// Force the $type to be written unconditionally by passing typeof(object) as the type being serialized.
serializer.Serialize(writer, value, typeof(object));
}
}
}
public struct PushValue<T> : IDisposable
{
Action<T> setValue;
T oldValue;
public PushValue(T value, Func<T> getValue, Action<T> setValue)
{
if (getValue == null || setValue == null)
throw new ArgumentNullException();
this.setValue = setValue;
this.oldValue = getValue();
setValue(value);
}
#region IDisposable Members
// By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class.
public void Dispose()
{
if (setValue != null)
setValue(oldValue);
}
#endregion
}
Then in startup you would add this converter to the default JsonSerializer, passing in the assemblies for which you want "$type" applied.
Update
If for whatever reason it's inconvenient to pass the list of assemblies in at startup, you could enable the converter by objectType.Namespace. All types living in your specified namespaces would automatically get serialized with TypeNameHandling.Auto.
Alternatively, you could introduce an Attribute which targets an assembly, class or interface and enables TypeNameHandling.Auto when combined with the appropriate converter:
public class EnableJsonTypeNameHandlingConverter : JsonConverter
{
[ThreadStatic]
static bool disabled;
// Disables the converter in a thread-safe manner.
bool Disabled { get { return disabled; } set { disabled = value; } }
public override bool CanWrite { get { return !Disabled; } }
public override bool CanRead { get { return !Disabled; } }
public override bool CanConvert(Type objectType)
{
if (Disabled)
return false;
if (objectType.Assembly.GetCustomAttributes<EnableJsonTypeNameHandlingAttribute>().Any())
return true;
if (objectType.GetCustomAttributes<EnableJsonTypeNameHandlingAttribute>(true).Any())
return true;
foreach (var type in objectType.GetInterfaces())
if (type.GetCustomAttributes<EnableJsonTypeNameHandlingAttribute>(true).Any())
return true;
return false;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
using (new PushValue<bool>(true, () => Disabled, val => Disabled = val)) // Prevent infinite recursion of converters
using (new PushValue<TypeNameHandling>(TypeNameHandling.Auto, () => serializer.TypeNameHandling, val => serializer.TypeNameHandling = val))
{
return serializer.Deserialize(reader, objectType);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
using (new PushValue<bool>(true, () => Disabled, val => Disabled = val)) // Prevent infinite recursion of converters
using (new PushValue<TypeNameHandling>(TypeNameHandling.Auto, () => serializer.TypeNameHandling, val => serializer.TypeNameHandling = val))
{
// Force the $type to be written unconditionally by passing typeof(object) as the type being serialized.
serializer.Serialize(writer, value, typeof(object));
}
}
}
[System.AttributeUsage(System.AttributeTargets.Assembly | System.AttributeTargets.Class | System.AttributeTargets.Interface)]
public class EnableJsonTypeNameHandlingAttribute : System.Attribute
{
public EnableJsonTypeNameHandlingAttribute()
{
}
}
Note - tested with various test cases but not SignalR itself since I don't currently have it installed.
TypeNameHandling Caution
When using TypeNameHandling, do take note of this caution from the Newtonsoft docs:
TypeNameHandling should be used with caution when your application deserializes JSON from an external source. Incoming types should be validated with a custom SerializationBinder when deserializing with a value other than None.
For a discussion of why this may be necessary, see TypeNameHandling caution in Newtonsoft Json.
I know that this is a rather old thread and that there is an accepted answer.
However, I had the problem that I could not make the Server read the received json correctly, that is it did only read the base classes
However, the solution to the problem was quite simple:
I added this line before the parameter classes:
[JsonConverter(typeof(PolymorphicAssemblyRootConverter), typeof(ABase))]
public class ABase
{
}
public class ADerived : ABase
{
public AInner[] DifferentObjects { get; set;}
}
public class AInner
{
}
public class AInnerDerived : AInner
{
}
...
public class PolymorphicAssemblyRootConverter: JsonConverter
{
public PolymorphicAssemblyRootConverter(Type classType) :
this(new Assembly[]{classType.Assembly})
{
}
// Here comes the rest of PolymorphicAssemblyRootConverter
}
No need to set JsonSerializer on the proxy connection of the client and add it to the GlobalHost.DependencyResolver.
It took me a long time to figure it out, I am using SignalR 2.2.1 on both client and server.
It's easier that your thought. I came across the same issue, trying to serialize derived classes however no properties from derived types are sent.
As Microsoft says here: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-how-to?pivots=dotnet-5-0#serialize-properties-of-derived-classes
If you specify your model of type "Object" instead of the strongly typed "Base type" it will be serialized as such and then properties will be sent.
If you have a big graph of objects you need to to it all the way down. It violates the strongly typed (type safety) but it allows the technology to send the data back with no changes to your code, just to your model.
as an example:
public class NotificationItem
{
public string CreatedAt { get; set; }
}
public class NotificationEventLive : NotificationItem
{
public string Activity { get; set; }
public string ActivityType { get; set;}
public DateTime Date { get; set;}
}
And if your main model that uses this Type is something like:
public class UserModel
{
public string Name { get; set; }
public IEnumerable<object> Notifications { get; set; } // note the "object"
..
}
if you try
var model = new UserModel() { ... }
JsonSerializer.Serialize(model);
you will sent all your properties from your derived types.
The solution is not perfect because you lose the strongly typed model, but if this is a ViewModel being passed to javascript which is in case of SignalR usage it works just fine.