Deserializing empty xml attribute value into nullable int property using XmlSerializer
Asked Answered
G

7

87

I get an xml from the 3rd party and I need to deserialize it into C# object. This xml may contain attributes with value of integer type or empty value: attr=”11” or attr=””. I want to deserialize this attribute value into the property with type of nullable integer. But XmlSerializer does not support deserialization into nullable types. The following test code fails during creation of XmlSerializer with InvalidOperationException {"There was an error reflecting type 'TestConsoleApplication.SerializeMe'."}.

[XmlRoot("root")]
public class SerializeMe
{
    [XmlElement("element")]
    public Element Element { get; set; }
}

public class Element
{
    [XmlAttribute("attr")]
    public int? Value { get; set; }
}

class Program {
    static void Main(string[] args) {
        string xml = "<root><element attr=''>valE</element></root>";
        var deserializer = new XmlSerializer(typeof(SerializeMe));
        Stream xmlStream = new MemoryStream(Encoding.ASCII.GetBytes(xml));
        var result = (SerializeMe)deserializer.Deserialize(xmlStream);
    }
}

When I change type of 'Value' property to int, deserialization fails with InvalidOperationException:

There is an error in XML document (1, 16).

Can anybody advise how to deserialize attribute with empty value into nullable type (as a null) at the same time deserializing non-empty attribute value into the integer? Is there any trick for this so I will not have to do deserialization of each field manually (actually there are a lot of them)?

Update after comment from ahsteele:

  1. Xsi:nil attribute

    As far as I know, this attribute works only with XmlElementAttribute - this attribute specifies that the element has no content, whether child elements or body text. But I need to find the solution for XmlAttributeAttribute. Anyway I cannot change xml because I have no control over it.

  2. bool *Specified property

    This property works only when attribute value is non-empty or when attribute is missing. When attr has empty value (attr='') the XmlSerializer constructor fails (as expected).

    public class Element
    {
        [XmlAttribute("attr")]
        public int Value { get; set; }
    
        [XmlIgnore]
        public bool ValueSpecified;
    }
    
  3. Custom Nullable class like in this blog post by Alex Scordellis

    I tried to adopt the class from this blog post to my problem:

    [XmlAttribute("attr")]
    public NullableInt Value { get; set; } 
    

    But XmlSerializer constructor fails with InvalidOperationException:

    Cannot serialize member 'Value' of type TestConsoleApplication.NullableInt.

    XmlAttribute/XmlText cannot be used to encode types implementing IXmlSerializable }

  4. Ugly surrogate solution (shame on me that I wrote this code here :) ):

    public class Element
    {
        [XmlAttribute("attr")]
        public string SetValue { get; set; }
    
        public int? GetValue()
        {
            if ( string.IsNullOrEmpty(SetValue) || SetValue.Trim().Length <= 0 )
                return null;
    
            int result;
            if (int.TryParse(SetValue, out result))
                return result;
    
            return null;
        }
    }
    

    But I don’t want to come up with the solution like this because it breaks interface of my class for its consumers. I would better manually implement IXmlSerializable interface.

Currently it looks like I have to implement IXmlSerializable for the whole Element class (it is big) and there are no simple workaround…

Grethel answered 18/8, 2009 at 18:35 Comment(0)
G
24

I solved this problem by implementing IXmlSerializable interface. I did not found easier way.

Here is the test code sample:

[XmlRoot("root")]
public class DeserializeMe {
    [XmlArray("elements"), XmlArrayItem("element")]
    public List<Element> Element { get; set; }
}

public class Element : IXmlSerializable {
    public int? Value1 { get; private set; }
    public float? Value2 { get; private set; }

    public void ReadXml(XmlReader reader) {
        string attr1 = reader.GetAttribute("attr");
        string attr2 = reader.GetAttribute("attr2");
        reader.Read();

        Value1 = ConvertToNullable<int>(attr1);
        Value2 = ConvertToNullable<float>(attr2);
    }

    private static T? ConvertToNullable<T>(string inputValue) where T : struct {
        if ( string.IsNullOrEmpty(inputValue) || inputValue.Trim().Length == 0 ) {
            return null;
        }

        try {
            TypeConverter conv = TypeDescriptor.GetConverter(typeof(T));
            return (T)conv.ConvertFrom(inputValue);
        }
        catch ( NotSupportedException ) {
            // The conversion cannot be performed
            return null;
        }
    }

    public XmlSchema GetSchema() { return null; }
    public void WriteXml(XmlWriter writer) { throw new NotImplementedException(); }
}

class TestProgram {
    public static void Main(string[] args) {
        string xml = @"<root><elements><element attr='11' attr2='11.3'/><element attr='' attr2=''/></elements></root>";
        XmlSerializer deserializer = new XmlSerializer(typeof(DeserializeMe));
        Stream xmlStream = new MemoryStream(Encoding.ASCII.GetBytes(xml));
        var result = (DeserializeMe)deserializer.Deserialize(xmlStream);
    }
}
Grethel answered 20/8, 2009 at 9:18 Comment(0)
T
74

This should work:

[XmlIgnore]
public int? Age { get; set; }

[XmlElement("Age")]
public string AgeAsText
{
  get { return (Age.HasValue) ? Age.ToString() : null; } 
  set { Age = !string.IsNullOrEmpty(value) ? int.Parse(value) : default(int?); }
}
Tamaratamarack answered 25/9, 2009 at 19:36 Comment(3)
This will work, but this is the same solution as number 4) from my question. I don't want to introduce surrogate fields into public interface of my class. ThanksGrethel
FWIW, I find this solution to be better than the explicit IXmlSerializable implementation (the accepted solution), although not to the OP's specific question. I avoid implementing IXmlSerializable unless absolutely required, finding that it winds up costing me more in maintenance over the long haul. In a simple case such as this and without any other mitigating factors, I will go for the "ugly" surrogate solution without giving it a second thought.Photoluminescence
gives a bit of extra overhead, but you could of course have two classes - one for deserialization which has all these extra properties, and the other which only has the actual values. Create an implicit conversion that just returns a new instance of the class being converted to with all the correct info.Fadein
G
24

I solved this problem by implementing IXmlSerializable interface. I did not found easier way.

Here is the test code sample:

[XmlRoot("root")]
public class DeserializeMe {
    [XmlArray("elements"), XmlArrayItem("element")]
    public List<Element> Element { get; set; }
}

public class Element : IXmlSerializable {
    public int? Value1 { get; private set; }
    public float? Value2 { get; private set; }

    public void ReadXml(XmlReader reader) {
        string attr1 = reader.GetAttribute("attr");
        string attr2 = reader.GetAttribute("attr2");
        reader.Read();

        Value1 = ConvertToNullable<int>(attr1);
        Value2 = ConvertToNullable<float>(attr2);
    }

    private static T? ConvertToNullable<T>(string inputValue) where T : struct {
        if ( string.IsNullOrEmpty(inputValue) || inputValue.Trim().Length == 0 ) {
            return null;
        }

        try {
            TypeConverter conv = TypeDescriptor.GetConverter(typeof(T));
            return (T)conv.ConvertFrom(inputValue);
        }
        catch ( NotSupportedException ) {
            // The conversion cannot be performed
            return null;
        }
    }

    public XmlSchema GetSchema() { return null; }
    public void WriteXml(XmlWriter writer) { throw new NotImplementedException(); }
}

class TestProgram {
    public static void Main(string[] args) {
        string xml = @"<root><elements><element attr='11' attr2='11.3'/><element attr='' attr2=''/></elements></root>";
        XmlSerializer deserializer = new XmlSerializer(typeof(DeserializeMe));
        Stream xmlStream = new MemoryStream(Encoding.ASCII.GetBytes(xml));
        var result = (DeserializeMe)deserializer.Deserialize(xmlStream);
    }
}
Grethel answered 20/8, 2009 at 9:18 Comment(0)
F
13

I've been messing around with serialization a lot myself of late and have found the following articles and posts helpful when dealing with null data for value types.

The answer to How to make a value type nullable with XmlSerializer in C# - serialization details a pretty nifty trick of the XmlSerializer. Specifically, the XmlSerialier looks for a XXXSpecified boolean property to determine if it should be included which allows you to ignore nulls.

Alex Scordellis asked a StackOverflow question which received a good answer. Alex also did a good writeup on his blog about the problem he was trying to solve Using XmlSerializer to deserialize into a Nullable<int>.

The MSDN documentation on Xsi:nil Attribute Binding Support is also useful. As is the documentation on IXmlSerializable Interface, though writing your own implementation should be your last resort.

Flickinger answered 18/8, 2009 at 18:57 Comment(2)
The "Using XmlSerializer to deserialize into a Nullable" link is dead. Here's a cached version from googleAutobahn
@Autobahn I switched the link in the answer to the Wayback Machine archive of the original Using XmlSerializer to deserialize into a Nullable<int>.Flickinger
G
3

Thought I might as well throw my answer into the hat: Solved this issue by creating a custom type that implements the IXmlSerializable interface:

Say you have a an XML object with the following nodes:

<ItemOne>10</Item2>
<ItemTwo />

The object to represent them:

public class MyItems {
    [XmlElement("ItemOne")]
    public int ItemOne { get; set; }

    [XmlElement("ItemTwo")]
    public CustomNullable<int> ItemTwo { get; set; } // will throw exception if empty element and type is int
}

Dynamic nullable struct to represent any potential nullable entries along with conversion

public struct CustomNullable<T> : IXmlSerializable where T: struct {
    private T value;
    private bool hasValue;

    public bool HasValue {
        get { return hasValue; }
    }

    public T Value {
        get { return value; }
    }

    private CustomNullable(T value) {
        this.hasValue = true;
        this.value = value;
    }

    public XmlSchema GetSchema() {
        return null;
    }

    public void ReadXml(XmlReader reader) {
        string strValue = reader.ReadString();
        if (String.IsNullOrEmpty(strValue)) {
            this.hasValue = false;
        }
        else {
            T convertedValue = strValue.To<T>();
            this.value = convertedValue;
            this.hasValue = true;
        }
        reader.ReadEndElement();

    }

    public void WriteXml(XmlWriter writer) {
        throw new NotImplementedException();
    }

    public static implicit operator CustomNullable<T>(T value) {
        return new CustomNullable<T>(value);
    }

}

public static class ObjectExtensions {
    public static T To<T>(this object value) {
        Type t = typeof(T);
        // Get the type that was made nullable.
        Type valueType = Nullable.GetUnderlyingType(typeof(T));
        if (valueType != null) {
            // Nullable type.
            if (value == null) {
                // you may want to do something different here.
                return default(T);
            }
            else {
                // Convert to the value type.
                object result = Convert.ChangeType(value, valueType);
                // Cast the value type to the nullable type.
                return (T)result;
            }
        }
        else {
            // Not nullable.
            return (T)Convert.ChangeType(value, typeof(T));
        }
    }
}
Grayish answered 16/1, 2017 at 23:12 Comment(1)
This is fantastic, from a code readability point of view it's a lot nicer to use this than to litter your code with deserialising to strings and then using properties to cast. I can't speak to performance or best practices but it's keeping the code neat and if I find a property that the XSD didn't declare as nullable but actually is it's a lot easier to add this in. I had to tweak the ReadXML slightly and add if (reader.IsEmptyElement) { return; } just before the end for self closing elements, I hope that works properly.Mitchelmitchell
S
2

You can also do this by loading the xml into an XmlDocument and then deserializing this into Json to get the object T that you are looking for.

        public static T XmlToModel<T>(string xml)
        {

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(xml);

            string jsonText = JsonConvert.SerializeXmlNode(doc);

            T result = JsonConvert.DeserializeObject<T>(jsonText);

            return result;
        }

Saransk answered 30/9, 2020 at 9:28 Comment(2)
Copy&pasted answer from linkConsignee
Looks like a good idea, the problem is that create new problems, for example deserializing bool (can't deserialize 0 or 1 as boolean). I downvote because this warning should be present in this answer.Downstream
P
1

You need two classes. The first class is your deserialization class. Any nullable properties should be strings.

You decorate it like this:

[XmlAttribute("my_prop_from_xml")]
public string MyPropFromXML { get; set; }

It handles your deserialization.

Your second class is for your project/db. It's property looks something like this:

public bool? MyPropForProject { get; set; }

You map with automapper or with another strategy doing this:

MyPropForProject = m.MyPropFromXML.ToNullableBool();

This keeps your logic confined and your classes dedicated to what they should do.

Here's an example for .ToNullableBool():

public static bool? ToNullableBool(this string str)
{
    if (bool.TryParse(str, out bool b))
    {
        return b;
    }

    if (int.TryParse(str, out int i))
    {
        if (i == 0)
        {
            return false;
        }
        else if (i == 1)
        {
            return true;
        }
    }

    return null;
}

Obviously, you can ignore this advice, assign a field/property with XmlIgnore, a setter and a getter and just call this extension class when setting to the field... but that's entirely up to you.

Phase answered 20/12, 2023 at 20:49 Comment(0)
C
0

I needed to deserialize xml that could not be modified, it lacked some attributes, I needed to make them nullable.

I have been looking for a simple and working solution for a long time, but unfortunately there is no such solution. I had to write my own implementation of IXmlSerializable for a class with nullable fields.

#nullable enable
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.Xml.XPath;
    
public abstract class XmlNullableSerializable : IXmlSerializable
{
    private static ConcurrentDictionary<int, XmlSerializer> SerializerCache { get; } = new();

    public virtual void ReadXml(XmlReader reader)
    {
        var xmlObject = XElement.Parse(reader.ReadOuterXml());
        var props = GetType().GetProperties().Where(x => x.CanWrite);

        foreach (var prop in props)
        {
            switch (prop.PropertyType)
            {
                case { } type when IsEnumerableType(type):
                {
                    var propName = GetXmlElementName(prop);
                    var xmlProps = xmlObject.XPathSelectElements(propName).ToList();

                    var value = GetValueFromEnumerableType(xmlProps, prop);
                    prop.SetValue(this, value ?? prop.GetValue(this), null);

                    break;
                }
                case { IsClass: true }:
                {
                    var propName = GetXmlElementName(prop);
                    var xmlProp = xmlObject.XPathSelectElement(propName);

                    var value = GetValueFromRefType(xmlProp, prop);
                    prop.SetValue(this, value ?? prop.GetValue(this), null);

                    break;
                }
                case { IsValueType: true }:
                {
                    var attrName = GetXmlAttrName(prop);
                    var xmlAttr = xmlObject.Attributes().FirstOrDefault(x => x.Name == attrName);

                    var value = GetValueFromValueType(xmlAttr, prop);
                    prop.SetValue(this, value ?? prop.GetValue(this), null);

                    break;
                }
                default: throw new NotImplementedException($"Type {prop.PropertyType} from {prop} not support");
            }
        }
    }

    private static object? GetValueFromValueType(XAttribute? xmlAttr, PropertyInfo prop)
    {
        var underlyingType = Nullable.GetUnderlyingType(prop.PropertyType);
        var isNullable = underlyingType != null;
        var type = underlyingType ?? prop.PropertyType;

        var result = xmlAttr?.Value switch
        {
            null when isNullable => null,
            { } value when typeof(Guid) == type => TypeDescriptor.GetConverter(type).ConvertFromInvariantString(value),
            { } value when typeof(bool) == type => value switch
            {
                _ when string.IsNullOrEmpty(value) => default,
                "1" => true,
                "0" => false,
                _ when bool.TryParse(value, out var boolResult) => boolResult,
                _ => default
            },
            { } value when type.IsEnum => EnumConvertor(value, type),
            _ => xmlAttr?.Value is null ? null : Convert.ChangeType(xmlAttr.Value, type)
        };

        return result;
    }
    
    private static object? GetValueFromRefType(XNode? xmlProp, PropertyInfo prop)
    {
        if (xmlProp is null) return null;
        using var propReader = xmlProp.CreateReader();
        var serializer = new XmlSerializer(prop.PropertyType);
        return serializer.Deserialize(propReader);
    }
    
    private static object? GetValueFromEnumerableType(List<XElement> xmlElements, PropertyInfo prop)
    {
        if (!xmlElements.Any()) return null;

        var sb = new StringBuilder();
        var type = typeof(EnumerableWrapper<>).MakeGenericType(prop.PropertyType);

        sb.AppendLine($"<{nameof(EnumerableWrapper<object>.Items)}>");
        foreach (var xmlProp in xmlElements)
            sb.AppendLine(xmlProp.ToString());
        sb.AppendLine($"</{nameof(EnumerableWrapper<object>.Items)}>");

        using var arrayXmlReader = new StringReader(sb.ToString());

        var overrides = new XmlAttributeOverrides();
        overrides.Add(type, nameof(EnumerableWrapper<object>.Items), new XmlAttributes()
        {
            XmlElements =
            {
                new XmlElementAttribute()
                {
                    ElementName = xmlElements[0].Name.LocalName,
                }
            }
        });

        var serializer = GetXmlSerializer(type, overrides);

        var result = serializer.Deserialize(arrayXmlReader);
        return result is null
            ? null
            : type.GetProperty(nameof(EnumerableWrapper<object>.Items))!.GetValue(result, null);
    }

    private static object EnumConvertor(object? value, Type type)
    {
        if (value is string s)
            value = Enum.Parse(type, s, true);

        value = Enum.ToObject(type, Convert.ToUInt64(value));

        if (!Enum.IsDefined(type, value))
            throw new InvalidCastException($"Cannot cast {value} to enum type {type.Name}.");

        return value;
    }
    
    private static XmlSerializer GetXmlSerializer(Type type, XmlAttributeOverrides attributeOverrides)
    {
        var key =
            $"{type.FullName!}-{attributeOverrides[type, nameof(EnumerableWrapper<object>.Items)].XmlElements[0].ElementName}"
                .GetHashCode();
        
        if (SerializerCache.TryGetValue(key, out var serializer)) return serializer;
        serializer = new XmlSerializer(type, attributeOverrides);
        SerializerCache.AddOrUpdate(key, x => serializer, (i, xmlSerializer) => serializer);
        return serializer;
    }
    
    private static bool IsEnumerableType(Type type)
    {
        return type.Name != nameof(String)
               && type.GetInterface(nameof(IEnumerable)) != null;
    }

    protected virtual string GetXmlElementName(PropertyInfo prop)
    {
        var xmlElement =
            prop.GetCustomAttributes(typeof(XmlElementAttribute), true).FirstOrDefault() as XmlElementAttribute;

        return xmlElement?.ElementName ?? prop.Name;
    }

    protected virtual string GetXmlAttrName(PropertyInfo prop)
    {
        var xmlAttribute =
            prop.GetCustomAttributes(typeof(XmlAttributeAttribute), true).FirstOrDefault() as XmlAttributeAttribute;

        return xmlAttribute?.AttributeName ?? prop.Name;
    }

    public virtual XmlSchema GetSchema() => throw new NotImplementedException();
    public virtual void WriteXml(XmlWriter writer) => throw new NotImplementedException();
}

[XmlRoot(nameof(Items))]
public class EnumerableWrapper<T>
{
    public T Items { get; set; }
}

Then you just inherit your class from XmlNullableSerializable. This only works for deserialization, your class can contain nullable fields of type struct. Example:

public class A : XmlNullableSerializable
{
    public int Int {get;set;}
    public bool? Bool {get;set;}
}

public class B : XmlNullableSerializable
{
    public A Obj {get;set;}
    public List<A> Enumerable {get;set;}
    public DateTime? Dt {get;set;}
}

var xml = "...";
using var sr = new StringReader(xml);
var serializer = new XmlSerializer(typeof(B));
var obj = serializer.Deserialize(sr);
Cavalier answered 6/10, 2023 at 11:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.