Why does XmlSerializer fail to serialize enum value in .Net Core but works fine in .NET Framework
This breaking change is due to a difference in implementations in XmlSerializationWriter.WriteTypedPrimitive(string name, string ns, object o, bool xsiType)
between .NET Core and .NET Framework.
This can be seen in the following two demo fiddles:
.NET Core 3.1.0, which throws an exception as follows:
System.InvalidOperationException: There was an error generating the XML document. ---> System.InvalidOperationException: The type MyEnum may not be used in this context. at System.Xml.Serialization.XmlSerializationWriter.WriteTypedPrimitive(String name, String ns, Object o, Boolean xsiType)
.NET Framework 4.7.3460.0, which serializes a
new ValueContainer { Value = MyEnum.One }
as follows:<ValueContainer xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Value xsi:type="xsd:int">0</Value> </ValueContainer>
Note that, while XML is generated, information about the specific
enum
type present inValue
is not included, and instead only the underlying typeint
is shown in thexsi:type
attribute.
So, where does the difference arise? The full framework reference source can be seen here, and begins:
protected void WriteTypedPrimitive(string name, string ns, object o, bool xsiType) {
string value = null;
string type;
string typeNs = XmlSchema.Namespace;
bool writeRaw = true;
bool writeDirect = false;
Type t = o.GetType();
bool wroteStartElement = false;
switch (Type.GetTypeCode(t)) {
case TypeCode.String:
value = (string)o;
type = "string";
writeRaw = false;
break;
case TypeCode.Int32:
value = XmlConvert.ToString((int)o);
type = "int";
break;
Given that the incoming object o
is actually a boxed Enum.One
, then Type.GetTypeCode(Type type)
returns a TypeCode
appropriate for the underlying type of the enum, here TypeCode.Int32
, thus serializing your value successfully.
The current .Net core reference source is here and looks superficially similar:
protected void WriteTypedPrimitive(string name, string ns, object o, bool xsiType)
{
string value = null;
string type;
string typeNs = XmlSchema.Namespace;
bool writeRaw = true;
bool writeDirect = false;
Type t = o.GetType();
bool wroteStartElement = false;
switch (t.GetTypeCode())
{
case TypeCode.String:
value = (string)o;
type = "string";
writeRaw = false;
break;
case TypeCode.Int32:
value = XmlConvert.ToString((int)o);
type = "int";
break;
But wait - what is this method t.GetTypeCode()
? There is no instance method GetTypeCode()
on Type
so it must be some sort of extension method. But where? A quick search of the reference source turned up at least three different, inconsistent public static TypeCode GetTypeCode(this Type type)
methods:
System.Runtime.Serialization.TypeExtensionMethods.GetTypeCode(this Type type)
.System.Dynamic.Utils.TypeExtensions.GetTypeCode(this Type type)
.System.Xml.Serialization.TypeExtensionMethods.GetTypeCode(this Type type)
.Since
System.Xml.Serialization
is the namespace ofXmlSerializationWriter
I believe that this is the one used. And it doesn't callType.GetTypeCode()
:public static TypeCode GetTypeCode(this Type type) { if (type == null) { return TypeCode.Empty; } else if (type == typeof(bool)) { return TypeCode.Boolean; } else if (type == typeof(char)) { return TypeCode.Char; } else if (type == typeof(sbyte)) { return TypeCode.SByte; } else if (type == typeof(byte)) { return TypeCode.Byte; } else if (type == typeof(short)) { return TypeCode.Int16; } else if (type == typeof(ushort)) { return TypeCode.UInt16; } else if (type == typeof(int)) { return TypeCode.Int32; } else if (type == typeof(uint)) { return TypeCode.UInt32; } else if (type == typeof(long)) { return TypeCode.Int64; } else if (type == typeof(ulong)) { return TypeCode.UInt64; } else if (type == typeof(float)) { return TypeCode.Single; } else if (type == typeof(double)) { return TypeCode.Double; } else if (type == typeof(decimal)) { return TypeCode.Decimal; } else if (type == typeof(DateTime)) { return TypeCode.DateTime; } else if (type == typeof(string)) { return TypeCode.String; } else { return TypeCode.Object; } }
Thus when passed an
enum
type,TypeCode.Object
will be returned.
The replacement of System.Type.GetTypeCode(Type t)
with System.Xml.Serialization.TypeExtensionMethods.GetTypeCode(this Type type)
is the breaking change that is causing your serialization failure.
All this begs the question, is this breaking change a bug, or a bug fix?
XmlSerializer
is designed for round-tripping of serializable objects: it generally refuses to serialize any type that it cannot also deserialize without data loss. But in your case, data is being lost, as enum
values are getting degraded into integer values. So this behavior change may be intentional. Nevertheless, you could open an issue here asking whether the breaking change was intentional.
To avoid the exception, you should properly declare all expected enum
types (and other types) with [XmlInclude(typeof(TEnum))]
attributes on ValueContainer
:
[XmlInclude(typeof(MyEnum)), XmlInclude(typeof(SomeOtherEnum)), XmlInclude(typeof(SomeOtherClass)) /* Include all other expected custom types here*/]
public class ValueContainer
{
public object Value;
}
This is the intended way to serialize polymorphic members using XmlSerializer
, and ensures that type information is round-tripped. It works in both .NET Core and .NET Full Framework. For related questions, see Serializing a class with a generic Enum that can be different Enum types and Using XmlSerializer to serialize derived classes.
Demo fiddle #3 here.
The workarounds suggested in this answer by Eldar also avoid the exception but converting the enum
to an int
will cause loss of type information.