XML deserialization generic method - c#

I have next XML file:
<Root>
<Document>
<Id>d639a54f-baca-11e1-8067-001fd09b1dfd</Id>
<Balance>-24145</Balance>
</Document>
<Document>
<Id>e3b3b4cd-bb8e-11e1-8067-001fd09b1dfd</Id>
<Balance>0.28</Balance>
</Document>
</Root>
I deserialize it to this class:
[XmlRoot("Root", IsNullable = false)]
public class DocBalanceCollection
{
[XmlElement("Document")]
public List<DocBalanceItem> DocsBalanceItems = new List<DocBalanceItem>();
}
where DocBalanceItem is:
public class DocBalanceItem
{
[XmlElement("Id")]
public Guid DocId { get; set; }
[XmlElement("Balance")]
public decimal? BalanceAmount { get; set; }
}
Here is my deserialization method:
public DocBalanceCollection DeserializeDocBalances(string filePath)
{
var docBalanceCollection = new DocBalanceCollection();
if (File.Exists(filePath))
{
var serializer = new XmlSerializer(docBalanceCollection.GetType());
TextReader reader = new StreamReader(filePath);
docBalanceCollection = (DocBalanceCollection)serializer.Deserialize(reader);
reader.Close();
}
return docBalanceCollection;
}
All works fine but I have many XML files. Besides writing Item classes I have to write ItemCollection classes for each of them. And also I have to implement DeserializeItems method for each.
Can I deserialize my XML files without creating ItemCollection classes? And can I write single generic method to deserialize all of them?
The only solution that comes to mind - make an interface for all these classes. Any ideas?

You can deserialize a generic List<T> just fine with XmlSerializer. However, first you need to add the XmlType attribute to your DocBalanceItem so it knows how the list elements are named.
[XmlType("Document")]
public class DocBalanceItem
{
[XmlElement("Id")]
public Guid DocId { get; set; }
[XmlElement("Balance")]
public decimal? BalanceAmount { get; set; }
}
Then modify your DeserializeDocBalances() method to return a List<T> and pass the serializer an XmlRootAttribute instance to instruct it to look for Root as the root element:
public List<T> DeserializeList<T>(string filePath)
{
var itemList = new List<T>();
if (File.Exists(filePath))
{
var serializer = new XmlSerializer(typeof(List<T>), new XmlRootAttribute("Root"));
TextReader reader = new StreamReader(filePath);
itemList = (List<T>)serializer.Deserialize(reader);
reader.Close();
}
return itemList;
}
Then you should be able to do
var list = DeserializeList<DocBalanceItem>("somefile.xml");
Since the method now returns a generic List<T>, you no longer need to create custom collections for every type.
P.S. - I tested this solution locally with the provided document, it does work.

Any stringable object can be deserialized by following method.
public static T genericDeserializeSingleObjFromXML<T>(T value, string XmalfileStorageFullPath)
{
T Tvalue = default(T);
try
{
XmlSerializer deserializer = new XmlSerializer(typeof(T));
TextReader textReader = new StreamReader(XmalfileStorageFullPath);
Tvalue = (T)deserializer.Deserialize(textReader);
textReader.Close();
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show("serialization Error : " + ex.Message);
}
return Tvalue;
}
In order to use this method you should already serialize the object in xml file.
Calling method is :
XmlSerialization.genericDeserializeSingleObjFromXML(new ObjectName(), "full path of the XML file");

Related

How can I be notified if I deserialize XML to C# and there's no corresponding C# property?

I'm working with some third party XML that has no formally defined schema, only example XML. I have several thousand XML files from this third party. There is no guarantee that every possible element lies within one or more of these files. The third party service could send me a new file with a new element!
I can view these files and reverse engineer types relatively easily.
For example:
<MyObject>
<MyProperty>Some value</MyProperty>
</MyObject>
Could deserialize to
public class MyObject
{
public string MyProperty { get; set; }
}
No problems so far.
But what if I attempt to deserialize this:
<MyObject>
<MyProperty>Some value</MyProperty>
<MyOtherProperty>Some value</MyOtherProperty>
</MyObject>
into my class above? I want it to throw an exception, so I can be notified that my class does not accommodate MyOtherProperty.
Is there a way to do this?
I'd like to share the code I wrote using the accepted answer. Using the below utility method, I can deserialize without checks for unknown stuff, and with checks, by setting strict=true.
I hope readers find this useful!
public static T XmlDeserialize<T>(string xml, bool strict = false)
{
using (var stringReader = new StringReader(xml))
{
using (var xmlTextReader = new XmlTextReader(stringReader))
{
var xmlSerializer = new XmlSerializer(typeof(T));
if (strict)
{
var options = new XmlDeserializationEvents();
options.OnUnknownElement += (sender, args) =>
{
throw new XmlDeserializationException(xml, $"Unexpected Element {args.Element.LocalName} on line {args.LineNumber}.");
};
options.OnUnknownAttribute += (sender, args) =>
{
throw new XmlDeserializationException(xml, $"Unexpected Element: {args.Attr.LocalName} on line {args.LineNumber}.");
};
options.OnUnknownNode += (sender, args) =>
{
throw new XmlDeserializationException(xml, $"Unexpected Element: {args.LocalName} on line {args.LineNumber}.");
};
return (T)xmlSerializer.Deserialize(xmlTextReader, options);
}
return (T)xmlSerializer.Deserialize(xmlTextReader);
}
}
}
And that exception class I'm throwing looks like this:
public class XmlDeserializationException : Exception
{
public string Xml { get; private set; }
public XmlDeserializationException(
string xml, string message) : base (message)
{
Xml = xml;
}
}
I can check my logs and look up the line number in the actual xml. Works perfectly. Thanks, pfx.
The XmlSerializer has a Deserialize overload allowing to pass in an options element by which to hook to some events; eg. OnUnknownElement.
XmlDeserializationEvents options = new XmlDeserializationEvents();
options.OnUnknownElement += (sender, args) => {
XmlElement unknownElement = args.Element;
// throw an Exception with this info.
} ;
var o = serializer.Deserialize(xml, options) as MyObject;
The OnUnknowElement takes nested elements into account.
With the classes below
public class MyObject
{
public string MyProperty { get; set; }
public MyOtherObject Other { get; set; }
}
public class MyOtherObject
{
public string SomeProperty { get; set; }
}
and the following xml
<MyObject>
<MyProperty>Some value</MyProperty>
<Other>
<SomeProperty>...</SomeProperty>
<UFO>...</UFO>
</Other>
</MyObject>
The OnUnknowElement handler will trigger for the UFO element.
One way of doing so would be to use your current object model and create an XSD out of it. You can then check new files against that XSD and throw if it doesn't validate.
Extend your class with an XmlAnyElement container.
Any unknown elements will end up in that array.
After deserialization check whether that array is empty.
public class MyObject
{
[XmlAnyElement]
public XmlElement[] UnknownElements;
public string MyProperty { get; set; }
}

Setting an XML attribute in the model

I need to set an attribute in my XML. I require the following:
<finAccount version="1.00">
Here is my model so far
[XmlAttribute("version")]
[XmlType("finPOWERConnect")]
public class ApplicationData
{
public List<Account> Accounts;
}
[XmlType("finAccount")]
public class Account
{
//Account stuff
}
The following function serialises my object to xml using the above model.
public Boolean SerialiseObjectToXmlString(Object obj, ref string xml)
{
System.IO.MemoryStream ms = null;
bool Ok = true;
XmlSerializer xmlSerializer = null;
xml = "";
//Serialise
try
{
xmlSerializer = new XmlSerializer(obj.GetType());
ms = new MemoryStream();
xmlSerializer.Serialize(ms, obj);
xml = System.Text.Encoding.UTF8.GetString(ms.GetBuffer(), 0, (int)ms.Length);
ms.Close();
}
catch(Exception ex)
{
Ok = false;
xml = ex.Message;
}
finally
{
if (ms != null) ms.Dispose();
}
return Ok;
}
I have looked at several examples that set the attribute in the above method however I use this method all throughout the application. Is there a way to set the xml attribute (version="1.00) in the model?
Try this. Class should be XmlRoot and arrays should be XmlElement. The XmlElement avoids adding two layer of the same tag to code. Try without XmlElement and you will see the difference.
[XmlRoot("finPOWERConnect")]
public class ApplicationData
{
[XmlElement("finAccount")]
public List<Account> Accounts {get; set; }
}
[XmlRoot("finAccount")]
public class Account
{
[XmlAttribute("version")]
public string Version { get; set; }
//Account stuff
}
​

Cannot deserialize XML into a list using XML Deserializer

This follows on from my previous question Serialize list of interfaces using XML Serialization
public class MeterWalkOrder
{
public MeterWalkOrder()
{
Meters = new List<IMeter>();
}
public String Name { get; set; }
[XmlIgnore]
public List<IMeter> Meters { get; set; }
[XmlArrayItem(ElementName = "Meter")]
[XmlArray(ElementName = "Meters")]
public List<Meter> SerializableMeters
{
get
{
return Meters.Cast<Meter>().ToList();
}
set
{
Meters = new List<IMeter>(value);
}
}
}
public interface IMeter {
int MeterID { get; set; }
}
public class Meter : IMeter {
public int MeterID { get; set; }
public string SerialNumber { get; set; }
}
}
I am using the extension method below to deserialize the XML back into my object (ideally I would prefer the extension method to be off of object, but I not too comfortable with extension methods so I have left like this for now)...
public static class SerializationExtensions
{
public static T LoadFromXML<T>(this string xmlString)
{
T returnValue = default(T);
XmlSerializer serial = new XmlSerializer(typeof(T));
StringReader reader = new StringReader(xmlString);
object result = serial.Deserialize(reader);
if (result != null && result is T)
{
returnValue = ((T)result);
}
reader.Close();
return returnValue;
}
....However, when I give the XML below....
<?xml version="1.0"?>
<MeterWalkOrder xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Red Route</Name>
<Meters>
<Meter>
<MeterID>1</MeterID>
<SerialNumber>12345</SerialNumber>
</Meter>
<Meter>
<MeterID>2</MeterID>
<SerialNumber>SE</SerialNumber>
</Meter>
</Meters>
</MeterWalkOrder>
No meters are populated?
Does anyone know what could cause this problem? The XML is valid and SerializeableMeters is simply a property that reads from and writes to Meters but casting it as a concrete class due to the known issues with using interfaces in serialization
The problem is that XmlSerializer deserializes a property referring to a class implementing IList<T> in the following way:
It calls the getter to get the list. If null, it allocates a list and sets it via the setter. It holds onto the list in some local variable while reading it.
It deserializes each list element, and adds it to the list it is holding.
And that's it. It never calls the containing class's list property setter afterwards.
You can verify this by replacing your List<Meter> with an ObservableCollection<Meter>, and setting a debug listener for when the collection changes:
[XmlArrayItem(ElementName = "Meter")]
[XmlArray(ElementName = "Meters")]
public ObservableCollection<Meter> SerializableMeters
{
get
{
Debug.WriteLine("Returning proxy SerializableMeters");
var list = new ObservableCollection<Meter>(Meters.Cast<Meter>());
list.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(list_CollectionChanged);
return list;
}
set
{
Debug.WriteLine("Setting proxy SerializableMeters");
Meters = new List<IMeter>(value.Cast<IMeter>());
}
}
static void list_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
var collection = (IList<Meter>)sender;
Debug.WriteLine("Proxy collection changed to include : ");
foreach (var item in collection)
Debug.WriteLine(" " + item.ToString());
}
Doing so, you'll see the following debug output:
Returning proxy SerializableMeters
Returning proxy SerializableMeters
Returning proxy SerializableMeters
Returning proxy SerializableMeters
Proxy collection changed to include :
Meter: 1, 12345
Proxy collection changed to include :
Meter: 1, 12345
Meter: 2, SE
As you can see, the list is never set back.
Luckily, there's an easy alternative. If you return a proxy array instead of a proxy List, XmlSerializer will allocate the array itself, populate it, and set it via the setter -- which is just what you want!
[XmlArrayItem(ElementName = "Meter")]
[XmlArray(ElementName = "Meters")]
public Meter [] SerializableMeters
{
get
{
return Meters.Cast<Meter>().ToArray();
}
set
{
Meters = new List<IMeter>(value.Cast<IMeter>());
}
}
And then later
var meters = xml.LoadFromXML<MeterWalkOrder>();
Debug.Assert(meters.Meters.Count == 2); // No assert.

Serializing XML into object - "error in xml document (2,2)

Yes, I have read other threads on this subject but I am missing something:
I am trying to deserialize an XML document that, in part, contains logged SMS messages. The XML file takes the format of:
<reports>
<report>
<sms_messages>
<sms_message>
<number>4155554432</number>
<text>Hi! How are you?</text>
</sms_message>
<sms_message>
<number>4320988876</number>
<text>Hello!</text>
</sms_message>
</sms_messages>
</report>
</reports>
My code looks like:
[XmlType("sms_message")]
public class SMSMessage
{
[XmlElement("number")]
public string Number {get;set;}
[XmlElement("text")]
public string TheText {get;set;}
}
[XmlType("report")]
public class AReport
{
[XmlArray("sms_messages")]
public List<SMSMessage> SMSMessages = new List<SMSMessage>();
}
[XmlRoot(Namespace="www.mysite.com", ElementName="reports", DataType="string", IsNullable=true)]
public class AllReports
{
[XmlArray("reports")]
public List<AReport> AllReports = new List<AReport>();
}
I am trying to serialize it like this:
XmlSerializer deserializer = new XmlSerializer(typeof(AllReports));
TextReader tr = new StreamReader(this.tbXMLPath.text);
List<AllReports> reports;
reports = (List<AllReports>)deserializer.Deserialize(tr);
tr.close();
I get the error: "There is an error in XML document (2,2)" The inner exception states,<reports xmlns=''> was not expected.
I am sure it has something to do with the serializer looking for the namespace of the root node? Is my syntax correct above with the [XmlArray("reports")] ? I feel like something is amiss because "reports" is the root node and it contains a list of "report" items, but the decoration for the root node isn't right? This is my first foray into this area. Any help is greatly appreciated.
With a minimal change to your non-compilable code
XmlSerializer deserializer = new XmlSerializer(typeof(AllReports));
TextReader tr = new StreamReader(filename);
AllReports reports = (AllReports)deserializer.Deserialize(tr);
[XmlType("sms_message")]
public class SMSMessage
{
[XmlElement("number")]
public string Number { get; set; }
[XmlElement("text")]
public string TheText { get; set; }
}
[XmlType("report")]
public class AReport
{
[XmlArray("sms_messages")]
public List<SMSMessage> SMSMessages = new List<SMSMessage>();
}
[XmlRoot("reports")]
public class AllReports
{
[XmlElement("report")]
public List<AReport> Reports = new List<AReport>();
}

Can I make XmlSerializer ignore the namespace on deserialization?

Can I make XmlSerializer ignore the namespace (xmlns attribute) on deserialization so that it doesn't matter if the attribute is added or not or even if the attribute is bogus? I know that the source will always be trusted so I don't care about the xmlns attribute.
Yes, you can tell the XmlSerializer to ignore namespaces during de-serialization.
Define an XmlTextReader that ignores namespaces. Like so:
// helper class to ignore namespaces when de-serializing
public class NamespaceIgnorantXmlTextReader : XmlTextReader
{
public NamespaceIgnorantXmlTextReader(System.IO.TextReader reader): base(reader) { }
public override string NamespaceURI
{
get { return ""; }
}
}
// helper class to omit XML decl at start of document when serializing
public class XTWFND : XmlTextWriter {
public XTWFND (System.IO.TextWriter w) : base(w) { Formatting= System.Xml.Formatting.Indented;}
public override void WriteStartDocument () { }
}
Here's an example of how you would de-serialize using that TextReader:
public class MyType1
{
public string Label
{
set { _Label= value; }
get { return _Label; }
}
private int _Epoch;
public int Epoch
{
set { _Epoch= value; }
get { return _Epoch; }
}
}
String RawXml_WithNamespaces = #"
<MyType1 xmlns='urn:booboo-dee-doo'>
<Label>This document has namespaces on its elements</Label>
<Epoch xmlns='urn:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'>0</Epoch>
</MyType1>";
System.IO.StringReader sr;
sr= new System.IO.StringReader(RawXml_WithNamespaces);
var s1 = new XmlSerializer(typeof(MyType1));
var o1= (MyType1) s1.Deserialize(new NamespaceIgnorantXmlTextReader(sr));
System.Console.WriteLine("\n\nDe-serialized, then serialized again:\n");
XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
ns.Add("urn", "booboo-dee-doo");
s1.Serialize(new XTWFND(System.Console.Out), o1, ns);
Console.WriteLine("\n\n");
The result is like so:
<MyType1>
<Label>This document has namespaces on its elements</Label>
<Epoch>0</Epoch>
</MyType1>
If you expect no namespace, but the input has namespaces, then you can set
Namespaces = false
on your XmlTextReader.
Exdended Wolfgang Grinfeld answer (w/o exception handling):
public static Message Convert(XmlDocument doc)
{
Message obj;
using (TextReader textReader = new StringReader(doc.OuterXml))
{
using (XmlTextReader reader = new XmlTextReader(textReader))
{
reader.Namespaces = false;
XmlSerializer serializer = new XmlSerializer(typeof(Message));
obj = (Message)serializer.Deserialize(reader);
}
}
return obj;
}
Solved this by using XmlSerializer Deserialize to read from xml instead from stream. This way before xml is Deserialized, using Regex to remove xsi:type from the xml. Was doing this is Portable Class Library for Cross Platform, so did not had many other options :(. After this the deserialization seems to work fine.
Following code can help,
public static TClass Deserialize<TClass>(string xml) where TClass : class, new()
{
var tClass = new TClass();
xml = RemoveTypeTagFromXml(xml);
var xmlSerializer = new XmlSerializer(typeof(TClass));
using (TextReader textReader = new StringReader(xml))
{
tClass = (TClass)xmlSerializer.Deserialize(textReader);
}
return tClass;
}
public static string RemoveTypeTagFromXml(string xml)
{
if (!string.IsNullOrEmpty(xml) && xml.Contains("xsi:type"))
{
xml = Regex.Replace(xml, #"\s+xsi:type=""\w+""", "");
}
return xml;
}
Why try to make the XmlSerializer forget how XML works? It's a fact of XML that two elements with the same name but different namespaces are different elements.
If you want to process XML that has no namespaces, then you should pre-process it to remove the namespaces, and then pass it to the serializer.

Categories

Resources