You are correct that this is happening because Exception implements ISerializable and Json.NET supports this interface. Specifically, from the reference source, in the method Exception.GetObjectData(SerializationInfo info, StreamingContext context), the Exception type serializes the underlying field, not the property:
info.AddValue("Message", _message, typeof(String));
Microsoft may have done this because the Message property has a default value if the underlying field is not set:
public virtual String Message {
get {
if (_message == null) {
if (_className==null) {
_className = GetClassName();
}
return Environment.GetResourceString("Exception_WasThrown", _className);
} else {
return _message;
}
}
}
By serializing the field, not the property, an exception with the default message will have its visible message automatically shown in the CurrentUICulture of the receiving system when deserialized.
Thus, if you want the value of the message property to appear in the JSON, instead of the underlying field, you're going to need to override GetObjectData(). And, since AddValue() throws an exception if you try to add a value with the same name as a pre-existing value, and SerializationInfo has no SetValue() method to replace a current value, you're going to need to do something with a bit of code smell:
public class GrossException : Exception
{
public GrossException() : base("Eww, gross") { }
protected GrossException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}
public class BarfException : GrossException
{
public BarfException() : base() { }
protected BarfException(SerializationInfo info, StreamingContext context) : base(info, context) { }
public override string Message { get { return "BARF!!"; } }
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
var tempInfo = new SerializationInfo(GetType(), new FormatterConverter());
base.GetObjectData(tempInfo, context);
foreach (SerializationEntry entry in tempInfo)
{
if (entry.Name != "Message")
{
info.AddValue(entry.Name, entry.Value, entry.ObjectType);
}
}
info.AddValue("Message", Message);
}
}
As you can see, this solution violates the design of your inheritance hierarchy since the value of the underlying _message field is no longer the value required by the base class GrossException. But at least the JSON is pretty.
A better solution would be to modify the GrossException type to have a protected constructor in which the message can be specified:
public class GrossException : Exception
{
public GrossException() : base("Eww, gross") { }
protected GrossException(SerializationInfo info, StreamingContext context) : base(info, context) { }
protected GrossException(string message) : base(message) { }
}
Or, if you just want to see the overridden message in the JSON for debugging purposes (say, because you're logging exceptions), you could just add it to the serialization stream like so:
public class BarfException : GrossException
{
public BarfException() : base() { }
protected BarfException(SerializationInfo info, StreamingContext context) : base(info, context) { }
public override string Message { get { return "BARF!!"; } }
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("OverriddenMessage", Message);
}
}
Either solution avoids the code smell.