C# XmlSerializer DefaultAttribute property still serialized for nullables - c#

I want to reduce the clutter in serialization output, introducing default values for often-the-same or not-used properties.
But still, they are in the output. What am I doing wrong?
This should be complete (though not compile):
[Serializable]
public class MyClass
{
[DefaultValue(null)]
public string Alias { get; set; } = null;
[DefaultValue(false)]
public bool Deactivated { get; set; } = false;
[DefaultValue(null)]
public bool? MyNullable { get; set; } = null;
}
public static string SerializeFromObject<T>(this T toSerialize)
{
var xmlSerializer = new XmlSerializer(toSerialize.GetType());
using (StringWriter textWriter = new StringWriter())
{
xmlSerializer.Serialize(textWriter, toSerialize);
return textWriter.ToString();
}
}
var myClass = new MyClass();
var str = SerializeFromObject(myClass);
And here the xml output, still including the nullable:
<?xml version="1.0" encoding="utf-16"?>
<MyClass xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<MyNullable xsi:nil="true" />
</MyClass>
How to get rid of the nullable in the serialized xml?

Self's answer was pretty helpful, but in the end there were some issues so I didn't follow it. I will outline it here later on for posterity though so it doesn't get lost in the comments or through a link going offline.
My own solution:
use standard .net xml serialization, re-read serialized string into XElement, remove all "nil"s. And then .ToString() it again.
Definitely a medium-nice solution, so feel free to come up with something nicer.
Conditional serialization as suggested would mean too much additional code for me which I would like to avoid.
My solution also has the disadvantage that DefaultValues cannot be specified for the nullables, they are always omitted when being null. Which is fine for me though. I use a nullable when I have no default value.
/// <summary>
/// use for compact serializations
/// nullables that don't have a value are omitted (irrespecitve of DefaultValue!)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="toSerialize"></param>
/// <returns></returns>
public static string SerializeFromObject_NoNils<T>(T toSerialize)
{
var ele = Serialize<T>(toSerialize);
void removeNils(XNode node)
{
// recursion
if (node is XElement elem)
{
foreach (var child in elem.DescendantNodes())
removeNils(child);
//foreach (var child in elem.Descendants())
// removeNils(child);
}
// same level
while (node != null)
{
var nextnode = node.NextNode;
//if (node.)
if ((node as System.Xml.Linq.XElement)?.Attribute("{http://www.w3.org/2001/XMLSchema-instance}nil")?.Value == "true")
node.Remove();
node = nextnode;
}
}
removeNils(ele.FirstNode);
return ele.ToString();
}
If someone want to build on or improve Self's answer - which has the disadvantage, that the DefaultValue attribute seems not to work (it seems to work on default(type) rather than that attribute), here it is copy/pasted from his link, with an added empty namespace because the default .net deserialization stumbles across the DataSerializerContract namespace.
So, this is not my code, credit goes to user Self.
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using System.ComponentModel;
using System.Runtime.Serialization;
public class Program
{
public static void Main()
{
var myClass = new MyClass();
var str_dc = DataContract_SerializeFromObject(myClass);
str_dc.Dump();
var str_xml = SerializeFromObject(myClass);
str_xml.Dump();
}
public static string SerializeFromObject<T>( T toSerialize)
{
var xmlSerializer = new XmlSerializer(toSerialize.GetType());
using (StringWriter textWriter = new StringWriter())
{
xmlSerializer.Serialize(textWriter, toSerialize);
return textWriter.ToString();
}
}
public static string DataContract_SerializeFromObject<T>( T toSerialize)
{
var xmlSerializer = new DataContractSerializer(toSerialize.GetType());
using (var output = new StringWriter())
using (var writer = new XmlTextWriter(output) { Formatting = Formatting.Indented })
{
xmlSerializer.WriteObject(writer, toSerialize);
return output.GetStringBuilder().ToString();
}
}
}
[DataContract(Namespace = "")] // default namespace is not deserializable with standard functionality
[Serializable]
public class MyClass
{
[DataMember(EmitDefaultValue = false)] [DefaultValue(null)]
public string Alias { get; set; } = null;
[DataMember(EmitDefaultValue = false)] [DefaultValue(false)]
public bool Deactivated { get; set; } = false;
[DataMember(EmitDefaultValue = false)] [DefaultValue(null)]
public bool? MyNullable { get; set; } = null;
}

Related

Deserialize XML into object with dynamic child elements

I'm trying to deserialize some xml into a C# object. The trick is for the most part, I know what this object will look like. However, this is one child that has dynamic elements.
(here is an example)
<measurement>
<Time>2021-02-02</Time>
<ID>1</ID>
<LeftWheel>
<ValuesRead>
<DynamicValue>12.3</DynamicValue>
<DynamicValue2>2.3</DynamicValue2>
<DynamicValue4>1.3</DynamicValue4>
<DynamicValue3>10.3</DynamicValue3>
</ValuesRead>
</LeftWheel>
<RightWheel>
<ValuesRead>
<DynamicValue>12.3</DynamicValue>
<DynamicValue2>2.3</DynamicValue2>
<DynamicValue6>1.3</DynamicValue6>
<DynamicValue10>10.3</DynamicValue10>
</ValuesRead>
</RightWheel>
</measurement>
In this XML, Measurement, Time, and ID are always going to in the object.
The LeftWheel and RightWheel elements are always going to be there with ValuesRead, but the ValuesRead children are dynamic and can be anything.
I have tried making a C# object to reflect most the structure, and then using the XmlSerializer.UnknownElement to pick up the unknown elements in the ValuesRead element, but I cannot link it to the parent above to know if it is on the LeftWheel or RightWheel.
XmlSerializer serializer = new XmlSerializer(typeof(FVISSiteEvent));
serializer.UnknownElement += UnknownElementFound;
Is there a way I can define the LeftWheel and RightWheel classes to be dynamic for the serialization, while having the other classes not dynamic?
You should be able to use the UnknownElementFound event to manually handle these aspects of serialization. See: Serialize XML array of unknown element name
Other options could be to specify the types you expect to see as XmlElementAtrribute decorated properties and they will just be null if they aren’t deserialized.
There’s also the nuclear option of implementing IXmlSerializable in your class and taking full control of the deserialization.
Uses Custom Serializer :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.Linq;
using System.Xml.Schema;
namespace ConsoleApplication16
{
class Program
{
const string INPUT_FILENAME = #"c:\temp\test.xml";
const string OUTPUT_FILENAME = #"c:\temp\test1.xml";
static void Main(string[] args)
{
XmlReader reader = XmlReader.Create(INPUT_FILENAME);
XmlSerializer serializer = new XmlSerializer(typeof(Measurement));
Measurement measurement = (Measurement)serializer.Deserialize(reader);
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
XmlWriter writer = XmlWriter.Create(OUTPUT_FILENAME,settings);
serializer.Serialize(writer, measurement);
}
}
[XmlRoot("measurement")]
public class Measurement
{
public DateTime Time { get; set; }
public int ID { get; set; }
[XmlArray("LeftWheel")]
[XmlArrayItem("ValuesRead")]
public List<Wheel> leftWheel { get; set; }
[XmlArray("RightWheel")]
[XmlArrayItem("ValuesRead")]
public List<Wheel> rightWheel { get; set; }
}
public class Wheel : IXmlSerializable
{
List<decimal> values { get; set; }
// Xml Serialization Infrastructure
public void WriteXml(XmlWriter writer)
{
int count = 0;
XElement valuesRead = new XElement("ValuesRead");
for (int i = 0; i < values.Count; i++ )
{
valuesRead.Add(new XElement("ValuesRead" + (i == 0? "" : i.ToString()), values[i]));
}
writer.WriteRaw(valuesRead.ToString());
}
public void ReadXml(XmlReader reader)
{
XElement values = (XElement)XElement.ReadFrom(reader);
this.values = values.Elements().Where(x => x.Name.LocalName.StartsWith("DynamicValue")).Select(x => (decimal)x).ToList();
}
public XmlSchema GetSchema()
{
return (null);
}
}
}
Besides using custom Xml serialization to deserialize your xml file, here is one another approach using Cinchoo ETL - an open source library to handle it simple way (those open to try it!)
Define POCO Class
public class Measurement
{
public DateTime Time { get; set; }
public int ID { get; set; }
[ChoXPath("LeftWheel/ValuesRead/*")]
public double[] LeftWheel { get; set; }
[ChoXPath("RightWheel/ValuesRead")]
public dynamic RightWheel { get; set; }
}
Deserialize using ChoETL
using (var r = ChoXmlReader<Measurement>.LoadText(xml)
.WithXPath("/")
)
{
foreach (var rec in r)
rec.Print();
}
Sample fiddle: https://dotnetfiddle.net/KtNvra
Disclaimer: I'm author of this library.
I've managed to resolve this using a dynamic type when deserializing.
When I deserialize ValuesRead, it is a defined as a dynamic type.
When deserialized, it turns into an XmlNode and from there I iterate over the node use the Name and InnerText values to read all the data.

Parse exponential Value in Xml document

I have an issue when deserializing an object (which I cannot modify).
I receive an exponential value for certain xml element and for represent them in my class I used decimal value expect that when I deserialize the xml document it fails.
<fwdRate>-9.72316862724032E-05</fwdRate>
Is there any solution to represent this attribute other than create 2 attributes in my class to represent it (one string and the other a decimal value)?
Can I create a custom deserializtion class for decimal value?
private void ParseXML(string value)
{
XmlSerializer serializer = new XmlSerializer(typeof(SwapDataSynapseResult));
using (TextReader reader = new StringReader(value))
{
_result = serializer.Deserialize(reader) as SwapDataSynapseResult;
}
}
As Demand
using System;
using System.IO;
using System.Xml.Serialization;
[XmlRoot(ElementName = "result")]
public class Result
{
[XmlElement(ElementName = "fwdRate")]
public decimal FwdRate { get; set; }
}
public class Program
{
public static void Main()
{
string val = "<result><fwdRate>-9.72316862724032E-05</fwdRate></result>";
Result response = ParseXML(val);
}
static Result ParseXML(string value)
{
XmlSerializer serializer = new XmlSerializer(typeof(Result));
using (TextReader reader = new StringReader(value))
{
return serializer.Deserialize(reader) as Result;
}
}
}
In XML, decimal values are not allowed to use scientific (exponential) notation (See this link at the 'Restrictions' paragraph).
Either:
the value is indeed a floating point one: Put a float/double instead of a decimal in the code.
the XML is corrupted.
In the same way, in C#, by default, Decimal.Parse doesn't accept exponential representation.
You can override this behavior by implementing a new struct that wrap a decimal and implement IXmlSerializable and allow exponential representation when de-serialized:
public struct XmlDecimal : IXmlSerializable
{
public decimal Value { get; private set; }
public XmlDecimal(decimal value) => Value = value;
public XmlSchema GetSchema() => null;
public void ReadXml(XmlReader reader)
{
var s = reader.ReadElementContentAsString();
Value = decimal.TryParse(s, NumberStyles.Number | NumberStyles.AllowExponent,
NumberFormatInfo.InvariantInfo, out var value)
? value
: 0; // If parse fail the resulting value is 0. Maybe we can throw an exception here.
}
public void WriteXml(XmlWriter writer) => writer.WriteValue(Value);
public static implicit operator decimal(XmlDecimal v) => v.Value;
public override string ToString() => Value.ToString();
}
The flaw is that you have to use this struct instead of a decimal everywhere in your model.
And sadly you can't make this struct read-only, has explained here.
The proper way is to control how your properties are deserialized by implementing the IXmlSerializable interface:
IXmlSerializable
IXmlSerializable code project example
In the ReadXml method you should convert your number
var yourStringNumber = ...
this.fwdRate = Decimal.Parse(yourStringNumber, System.Globalization.NumberStyles.Float);
But this method will require you to parse of the whole xml manually that is a bit overhead sometimes.
A simple solution (that smells but might be useful) is just to add additional fwdRateDecimal field to your class and fulfill the value after the serialization.
private void ParseXML(string value)
{
XmlSerializer serializer = new XmlSerializer(typeof(SwapDataSynapseResult));
using (TextReader reader = new StringReader(value))
{
_result = serializer.Deserialize(reader) as SwapDataSynapseResult;
_result.fwdRateDecimal = Decimal.Parse(_result.fwdRate, System.Globalization.NumberStyles.Float)
}
}
Also conversion can be implemented in a type directly:
[XmlRoot(ElementName = "result")]
public class Result
{
[XmlElement(ElementName = "fwdRate")]
public string FwdRateStr { get; set; }
private string lastParsedValue = null;
private decimal fwdRate = 0;
[XmlIgnore]
public decimal FwdRate
{
get
{
if(FwdRateStr != lastParsedValue)
{
lastParsedValue = FwdRateStr
fwdRate = Decimal.Parse(FwdRateStr ,System.Globalization.NumberStyles.Float)
}
return fwdRate
}
}
This is the solution:
using System;
using System.IO;
using System.Xml.Serialization;
[XmlRoot(ElementName = "result")]
public class Result
{
[XmlElement(ElementName = "fwdRate")]
public double FwdRate { get; set; }
}
public class Program
{
public static void Main()
{
string val = "<result><fwdRate>-9.72316862724032E-05</fwdRate></result>";
Result response = ParseXML(val);
Console.WriteLine(response.FwdRate);
}
static Result ParseXML(string value)
{
XmlSerializer serializer = new XmlSerializer(typeof(Result));
using (TextReader reader = new StringReader(value))
{
return serializer.Deserialize(reader) as Result;
}
}
}
You can't modify the incoming xml but you can modify how you read its' data.
Read that number as a double instead of decimal and you will have the right decimal precision.
Output:
-9,72316862724032E-05

Serialize array of derived type property of derived type

I've got a simple .Net framework C# console app that serializes a a class that is of a derived type, where a property is also of a derived type.
The derived classes have names that are the same as the base class, but are in a different namespace to prevent them from clashing. It seems though that the reflection the XmlSerializer uses does not work with this too well. Maybe there is some way of wrangling the attributes that I can still end up with the base class using pretty names (as it will be a DLL interface when used) and the XML also using pretty names (as it will be human editable)... pretty names for the derived classes are not required (though would be a bonus).
The XML would hopefully look like:
<?xml version="1.0" encoding="utf-8"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Details>
<Detail>
<Description>bald</Description>
</Detail>
<Detail>
<Description>red tie</Description>
</Detail>
</Details>
</Person>
But the closest I can get without exceptions is where the <Detail> elements are
<Detail xsi:type="DerivedDetail"> ... </Detail>
Having to add this xs:type attribute is not the best for human-editable XML.
This is achieved with the below C# code. If I remove the marked XmlType attribute then the element should serialize without the xsi:type attribute, but instead I get an exception:
InvalidOperationException: Types 'Test.Detail' and 'Test.Xml.Detail' both use the XML type name, 'Detail', from namespace ''. Use XML attributes to specify a unique XML name and/or namespace for the type.
I tried marking the derived Xml.Detail class as an anonymous XML type but then the exception reads:
InvalidOperationException: Cannot include anonymous type 'Test.Xml.Detail'.
I have read many similar questions but have not encountered anything that solves this just yet.
In this code below Person is an abstract class that has a property that is an array of the abstract type Detail. These types are derived by Xml.Person and Xml.Detail respectively. The program creates a test Xml.Person object and attempts to serialize it:
using System;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace Test
{
class Program
{
static void Main(string[] args)
{
// Create test details array
var TestDetails = new Xml.Detail[]
{
new Xml.Detail
{
Description = "bald"
},
new Xml.Detail
{
Description = "red tie"
}
};
// create test person object that holds details array
var TestBar = new Xml.Person()
{
Details = TestDetails
};
// serialize the person object
var s = new Xml.Serializer();
var TestOutput = s.Serialize(TestBar);
Console.WriteLine(TestOutput);
}
}
// base classes
public abstract class Person
{
public abstract Detail[] Details { get; set; }
}
public abstract class Detail
{
public abstract string Description { get; set; }
}
namespace Xml
{
// derived classes
[Serializable]
[XmlType(AnonymousType = true)]
[XmlRoot(IsNullable = false)]
public class Person : Test.Person
{
[XmlArrayItem("Detail", typeof(Detail))]
[XmlArray(IsNullable = false)]
public override Test.Detail[] Details { get; set; }
}
// This attribute makes serialization work but also adds the xsi:type attribute
[XmlType("DerivedDetail")]
[Serializable]
public class Detail : Test.Detail
{
public override string Description { get; set; }
}
// class that does serializing work
public class Serializer
{
private static XmlSerializer PersonSerializer =
new XmlSerializer(typeof(Person), new Type[] { typeof(Detail) });
public string Serialize(Test.Person person)
{
string Output = null;
var Stream = new MemoryStream();
var Encoding = new UTF8Encoding(false, true);
using (var Writer = new XmlTextWriter(Stream, Encoding))
{
Writer.Formatting = Formatting.Indented;
PersonSerializer.Serialize(Writer, person);
Output = Encoding.GetString(Stream.ToArray());
}
Stream.Dispose();
return Output;
}
}
}
}
Not sure why you're using base classes instead of interfaces when you don't have any member fields. Regardless, I assumed you wanted Xml.Person to be a concrete instantiation of abstract Person or any classes derived from abstract Person without decorating abstract Person with XML attributes. I accomplished this by forcing abstract Person to become a concrete instantiation of Xml.Person before serializing it. Please replace XmlSerializationProject with Test.
using System;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
namespace XmlSerializationProject
{
class Program
{
static void Main(string[] args)
{
// Create test details array
var TestDetails = new Xml.Detail[]
{
new Xml.Detail
{
Description = "bald"
},
new Xml.Detail
{
Description = "red tie"
}
};
// create test person object that holds details array
var TestBar = new Xml.Person()
{
Details = TestDetails
};
// serialize the person object
var s = new Xml.Serializer();
var TestOutput = s.Serialize(TestBar);
Console.WriteLine(TestOutput);
Console.ReadKey();
}
}
// base classes
public abstract class Person
{
public abstract Detail[] Details { get; set; }
}
public abstract class Detail
{
public abstract string Description { get; set; }
}
namespace Xml
{
[Serializable]
[XmlType(AnonymousType = true)]
[XmlRoot(IsNullable = false)]
public class Person : XmlSerializationProject.Person
{
public Person()
{ }
public Person(XmlSerializationProject.Person person)
{
// Deep copy
if (person.Details == null) return;
this.Details = new Detail[person.Details.Length];
for (int i = 0; i < person.Details.Length; i++)
{
this.Details[i] = new Detail { Description = person.Details[i].Description };
}
}
[XmlArray(ElementName = "Details")]
[XmlArrayItem("Detail", typeof(Detail))]
[XmlArrayItem("ODetail", typeof(XmlSerializationProject.Detail))]
public override XmlSerializationProject.Detail[] Details
{
get;
set;
}
}
[Serializable]
public class Detail : XmlSerializationProject.Detail
{
public override string Description { get; set; }
}
// class that does serializing work
public class Serializer
{
private static readonly XmlSerializer PersonSerializer;
private static Serializer()
{
var xmlAttributeOverrides = new XmlAttributeOverrides();
// Change original "Detail" class's element name to "AbstractDetail"
var xmlAttributesOriginalDetail = new XmlAttributes();
xmlAttributesOriginalDetail.XmlType = new XmlTypeAttribute() { TypeName = "AbstractDetail" };
xmlAttributeOverrides.Add(typeof(XmlSerializationProject.Detail), xmlAttributesOriginalDetail);
// Ignore Person.Details array
var xmlAttributesOriginalDetailsArray = new XmlAttributes();
xmlAttributesOriginalDetailsArray.XmlIgnore = true;
xmlAttributeOverrides.Add(typeof(XmlSerializationProject.Person), "Details", xmlAttributesOriginalDetailsArray);
PersonSerializer = new XmlSerializer(
typeof(Person), xmlAttributeOverrides, new Type[] { typeof(Detail) }, new XmlRootAttribute(), "default");
}
public string Serialize(XmlSerializationProject.Person person)
{
return Serialize(new Person(person));
}
public string Serialize(Person person)
{
string Output = null;
var Stream = new MemoryStream();
var Encoding = new UTF8Encoding(false, true);
using (var Writer = new XmlTextWriter(Stream, Encoding))
{
Writer.Formatting = Formatting.Indented;
PersonSerializer.Serialize(Writer, person);
Output = Encoding.GetString(Stream.ToArray());
}
Stream.Dispose();
return Output;
}
}
}
}

How to parse JSON without JSON.NET library?

I'm trying to build a Metro application for Windows 8 on Visual Studio 2011.
and while I'm trying to do that, I'm having some issues on how to parse JSON without JSON.NET library (It doesn't support the metro applications yet).
Anyway, I want to parse this:
{
"name":"Prince Charming",
"artist":"Metallica",
"genre":"Rock and Metal",
"album":"Reload",
"album_image":"http:\/\/up203.siz.co.il\/up2\/u2zzzw4mjayz.png",
"link":"http:\/\/f2h.co.il\/7779182246886"
}
You can use the classes found in the System.Json Namespace which were added in .NET 4.5. You need to add a reference to the System.Runtime.Serialization assembly
The JsonValue.Parse() Method parses JSON text and returns a JsonValue:
JsonValue value = JsonValue.Parse(#"{ ""name"":""Prince Charming"", ...");
If you pass a string with a JSON object, you should be able to cast the value to a JsonObject:
using System.Json;
JsonObject result = value as JsonObject;
Console.WriteLine("Name .... {0}", (string)result["name"]);
Console.WriteLine("Artist .. {0}", (string)result["artist"]);
Console.WriteLine("Genre ... {0}", (string)result["genre"]);
Console.WriteLine("Album ... {0}", (string)result["album"]);
The classes are quite similar to those found in the System.Xml.Linq Namespace.
I use this...but have never done any metro app development, so I don't know of any restrictions on libraries available to you. (note, you'll need to mark your classes as with DataContract and DataMember attributes)
public static class JSONSerializer<TType> where TType : class
{
/// <summary>
/// Serializes an object to JSON
/// </summary>
public static string Serialize(TType instance)
{
var serializer = new DataContractJsonSerializer(typeof(TType));
using (var stream = new MemoryStream())
{
serializer.WriteObject(stream, instance);
return Encoding.Default.GetString(stream.ToArray());
}
}
/// <summary>
/// DeSerializes an object from JSON
/// </summary>
public static TType DeSerialize(string json)
{
using (var stream = new MemoryStream(Encoding.Default.GetBytes(json)))
{
var serializer = new DataContractJsonSerializer(typeof(TType));
return serializer.ReadObject(stream) as TType;
}
}
}
So, if you had a class like this...
[DataContract]
public class MusicInfo
{
[DataMember]
public string Name { get; set; }
[DataMember]
public string Artist { get; set; }
[DataMember]
public string Genre { get; set; }
[DataMember]
public string Album { get; set; }
[DataMember]
public string AlbumImage { get; set; }
[DataMember]
public string Link { get; set; }
}
Then you would use it like this...
var musicInfo = new MusicInfo
{
Name = "Prince Charming",
Artist = "Metallica",
Genre = "Rock and Metal",
Album = "Reload",
AlbumImage = "http://up203.siz.co.il/up2/u2zzzw4mjayz.png",
Link = "http://f2h.co.il/7779182246886"
};
// This will produce a JSON String
var serialized = JSONSerializer<MusicInfo>.Serialize(musicInfo);
// This will produce a copy of the instance you created earlier
var deserialized = JSONSerializer<MusicInfo>.DeSerialize(serialized);
For those who do not have 4.5, Here is my library function that reads json. It requires a project reference to System.Web.Extensions.
using System.Web.Script.Serialization;
public object DeserializeJson<T>(string Json)
{
JavaScriptSerializer JavaScriptSerializer = new JavaScriptSerializer();
return JavaScriptSerializer.Deserialize<T>(Json);
}
Usually, json is written out based on a contract. That contract can and usually will be codified in a class (T). Sometimes you can take a word from the json and search the object browser to find that type.
Example usage:
Given the json
{"logEntries":[],"value":"My Code","text":"My Text","enabled":true,"checkedIndices":[],"checkedItemsTextOverflows":false}
You could parse it into a RadComboBoxClientState object like this:
string ClientStateJson = Page.Request.Form("ReportGrid1_cboReportType_ClientState");
RadComboBoxClientState RadComboBoxClientState = DeserializeJson<RadComboBoxClientState>(ClientStateJson);
return RadComboBoxClientState.Value;
I needed a JSON serializer and deserializer without any 3rd party dependency or nuget, that can support old systems, so you don't have to choose between Newtonsoft.Json, System.Text.Json, DataContractSerializer, JavaScriptSerializer, etc. depending on the target platform.
So I have started this open source (MIT) project here:
https://github.com/smourier/ZeroDepJson
It's just one C# file ZeroDepJson.cs, compatible with .NET Framework 4.x to .NET Core, and .NET 5.
Note it's probably not as good as all the aforementioned libraries (especially in performance area), but it should be reasonably ok and friction-free.
Have you tried using JavaScriptSerializer ?
There's also DataContractJsonSerializer
I have written a small construct to do that long back, it requires System.Runtime.Serialization.Json namespace. It uses DataContractJsonSerializer to serialize & deserialize object with a static method JConvert.
It works with small set of data but haven't tested with big data source.
JsonHelper.cs
// Json Serializer without NewtonSoft
public static class JsonHelper
{
public static R JConvert<P,R>(this P t)
{
if(typeof(P) == typeof(string))
{
var return1 = (R)(JsonDeserializer<R>(t as string));
return return1;
}
else
{
var return2 = (JsonSerializer<P>(t));
R result = (R)Convert.ChangeType(return2, typeof(R));
return result;
}
}
private static String JsonSerializer<T>(T t)
{
var stream1 = new MemoryStream();
var ser = new DataContractJsonSerializer(typeof(T));
ser.WriteObject(stream1, t);
stream1.Position = 0;
var sr = new StreamReader(stream1);
return (sr.ReadToEnd());
}
private static T JsonDeserializer<T>(string jsonString)
{
T deserializedUser = default(T);
var ms = new MemoryStream(Encoding.UTF8.GetBytes(jsonString));
var ser = new DataContractJsonSerializer(typeof(T));
deserializedUser = (T)ser.ReadObject(ms);// as T;
ms.Close();
return deserializedUser;
}
}
Syntax:
To use the JsonHelper you need to call
JConvert<string,object>(str); //to Parse string to non anonymous <object>
JConvert<object,string>(obj); //to convert <obj> to string
Example:
Suppose we have a class person
public class person
{
public string FirstName {get;set;}
public string LastName {get;set;}
}
var obj = new person();//"vinod","srivastav");
obj.FirstName = "vinod";
obj.LastName = "srivastav";
To convert the person object we can call:
var asText = JsonHelper.JConvert<person,string>(obj); //to convert <obj> to string
var asObject = JsonHelper.JConvert<string,person>(asText); //to convert string to non-anonymous object
You can use DataContractJsonSerializer. See this link for more details.
using System;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;
namespace OTL
{
/// <summary>
/// Before usage: Define your class, sample:
/// [DataContract]
///public class MusicInfo
///{
/// [DataMember(Name="music_name")]
/// public string Name { get; set; }
/// [DataMember]
/// public string Artist{get; set;}
///}
/// </summary>
/// <typeparam name="T"></typeparam>
public class OTLJSON<T> where T : class
{
/// <summary>
/// Serializes an object to JSON
/// Usage: string serialized = OTLJSON<MusicInfo>.Serialize(musicInfo);
/// </summary>
/// <param name="instance"></param>
/// <returns></returns>
public static string Serialize(T instance)
{
var serializer = new DataContractJsonSerializer(typeof(T));
using (var stream = new MemoryStream())
{
serializer.WriteObject(stream, instance);
return Encoding.Default.GetString(stream.ToArray());
}
}
/// <summary>
/// DeSerializes an object from JSON
/// Usage: MusicInfo deserialized = OTLJSON<MusicInfo>.Deserialize(json);
/// </summary>
/// <param name="json"></param>
/// <returns></returns>
public static T Deserialize(string json)
{
if (string.IsNullOrEmpty(json))
throw new Exception("Json can't empty");
else
try
{
using (var stream = new MemoryStream(Encoding.Default.GetBytes(json)))
{
var serializer = new DataContractJsonSerializer(typeof(T));
return serializer.ReadObject(stream) as T;
}
}
catch (Exception e)
{
throw new Exception("Json can't convert to Object because it isn't correct format.");
}
}
}
}

A type of generic list deserialization class?

OK, so here's the story so far.
I could already deserialize individual objects using XmlSerializer, but deserializing lists was proving to be a real headache. I started out by trying to serialize List<Foo> and the serializer serialized multiple <Foo> XML structures inside a root <ArrayOfFoo> element. That proved to be problematic to deserialize, so it looks like I needed to have defined the 'ArrayOfFoo' element myself. So, I've got a class working that is a 'wrapper' for the list, as shown in this program:
using System;
using System.IO;
using System.Collections.Generic;
using System.Xml.Serialization;
namespace XmlTester2
{
public class Program
{
static void Main(string[] args)
{
Console.WriteLine("XML tester...");
string xml =
"<ItemList xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">" +
"<Person i:type=\"PersonI2\">" + "<Field1>field1Val</Field1>" +
"<Field2>field2Val</Field2>" + "<Field3>field3Val</Field3>" +
"<Field4>field4Val</Field4>" + "</Person>" +
"<Account i:type=\"AccountI2\">" + "<Field1>field1Val</Field1>" +
"<Field2>field2Val</Field2>" + "<Field3>field3Val</Field3>" +
"<Field4>field4Val</Field4>" + "</Account>" +
"<Person i:type=\"PersonI2\">" + "<Field1>field1Val</Field1>" +
"<Field2>field2Val</Field2>" + "<Field3>field3Val</Field3>" +
"<Field4>field4Val</Field4>" + "</Person>" + "</ItemList>";
XmlSerializer ser = new XmlSerializer(typeof(ItemList));
using (var reader = new StringReader(xml))
{
ItemList result = (ItemList)ser.Deserialize(reader);
}
Console.WriteLine("Break here and check 'result' in Quickwatch...");
Console.ReadKey();
}
}
[XmlRootAttribute(IsNullable = false)]
public class ItemList
{
[XmlElementAttribute("Person")]
public List<Person> Persons { get; set; }
[XmlElementAttribute("Account")]
public List<Account> Accounts { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "Person", Namespace = "")]
[XmlInclude(typeof(PersonI2))]
public class Person
{
public string Field1 { get; set; }
public string Field2 { get; set; }
public string Field3 { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "PersonI2", Namespace = "")]
public class PersonI2 : Person
{
public string Field4 { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "Account", Namespace = "")]
[XmlInclude(typeof(AccountI2))]
public class Account
{
public string Field1 { get; set; }
public string Field2 { get; set; }
public string Field3 { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "AccountI2", Namespace = "")]
public class AccountI2 : Account
{
public string Field4 { get; set; }
}
}
However, this 'wrapper', ItemList, still has to have manually defined in it all the elements that might be contained (in the example, Person and Account). What would be really ideal would be to have a generic list wrapper class. I know this is a bit hopeful, but would there be a way to do this? I'm thinking of something along these lines (this does not work, but is just to give you the general idea):
using System;
using System.IO;
using System.Collections.Generic;
using System.Xml.Serialization;
namespace XmlTester3
{
public class Program
{
static void Main(string[] args)
{
Console.WriteLine("XML tester...");
string xml =
"<ItemList xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">" +
"<Person i:type=\"PersonI2\">" +
"<Field1>field1Val</Field1>" +
"<Field2>field2Val</Field2>" +
"<Field3>field3Val</Field3>" +
"<Field4>field4Val</Field4>" +
"</Person>" +
"<Person i:type=\"PersonI2\">" +
"<Field1>field1Val</Field1>" +
"<Field2>field2Val</Field2>" +
"<Field3>field3Val</Field3>" +
"<Field4>field4Val</Field4>" +
"</Person>" +
"</ItemList>";
XmlSerializer ser = new XmlSerializer(typeof(ItemList<Person>));
using (var reader = new StringReader(xml))
{
ItemList<Person> result = (ItemList<Person>)ser.Deserialize(reader);
}
Console.WriteLine("Break here and check 'result' in Quickwatch...");
Console.ReadKey();
}
}
[XmlRootAttribute(IsNullable = false)]
[XmlInclude(typeof(Person))]
[XmlInclude(typeof(PersonI2))]
[XmlInclude(typeof(Account))]
[XmlInclude(typeof(AccountI2))]
public class ItemList<T>
{
[XmlElementAttribute]
public List<T> Items { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "Person", Namespace = "")]
[XmlInclude(typeof(PersonI2))]
public class Person
{
public string Field1 { get; set; }
public string Field2 { get; set; }
public string Field3 { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "PersonI2", Namespace = "")]
public class PersonI2 : Person
{
public string Field4 { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "Account", Namespace = "")]
[XmlInclude(typeof(AccountI2))]
public class Account
{
public string Field1 { get; set; }
public string Field2 { get; set; }
public string Field3 { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "AccountI2", Namespace = "")]
public class AccountI2 : Account
{
public string Field4 { get; set; }
}
}
So, the XML structures passed inside the ItemList would only be able to be of one type, say Person in this example, and I could define an ItemList<Person> that would allow me to deserialize a list containing multiple Person objects? Any ideas? If necessary, I wouldn't mind having to tag the ItemList class with an [XmlInclude...] for every type that ItemList might contain a collection of.
I'm guessing this is possible, I just haven't figured out quite how? :-) Or is the default XmlSerializer too fussy?
You can do this easily enough, just implement the System.Xml.Serialization.IXmlSerializable interface. If I were doing this, I might even reflect the possible derived types of T in the assembly that defines T and completely omit the [XmlInclude] declarations. The real down side with this approach is the creation of the XmlSerializers. You might consider caching them. Anyway just use this in your second example and it should work.
BTW, that is an interesting thing your doing with the "i:type=\"PersonI2\""; props for figuring that one out ;)
[XmlRootAttribute("ItemList", IsNullable = false)]
[XmlInclude(typeof(Person))]
[XmlInclude(typeof(PersonI2))]
[XmlInclude(typeof(Account))]
[XmlInclude(typeof(AccountI2))]
public class ItemList<T> : System.Xml.Serialization.IXmlSerializable
{
class Map : Dictionary<String, XmlSerializer>
{ public Map() : base(StringComparer.Ordinal) { } }
public List<T> Items { get; set; }
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
private string TypeName(Type t)
{
String typeName = t.Name;
foreach (XmlTypeAttribute a in t.GetCustomAttributes(typeof(XmlTypeAttribute), true))
if (!String.IsNullOrEmpty(a.TypeName))
typeName = a.TypeName;
return typeName;
}
private Map LoadSchema()
{
Map map = new Map();
foreach (XmlIncludeAttribute inc in typeof(ItemList<T>).GetCustomAttributes(typeof(XmlIncludeAttribute), true))
{
Type t = inc.Type;
if (typeof(T).IsAssignableFrom(t))
map.Add(TypeName(t), new XmlSerializer(t));
}
return map;
}
public void ReadXml(System.Xml.XmlReader reader)
{
Map map = LoadSchema();
int depth = reader.Depth;
List<T> items = new List<T>();
if (!reader.IsEmptyElement && reader.Read())
{
while (reader.Depth > depth)
{
items.Add((T)map[reader.LocalName].Deserialize(reader));
}
}
this.Items = items;
}
public void WriteXml(System.Xml.XmlWriter writer)
{
Map map = LoadSchema();
foreach (T item in this.Items)
{
map[TypeName(item.GetType())].Serialize(writer, item);
}
}
}
I'm not sure I understand your question but do you know there's a XmlArrayItemAttribute.
[XmlArray("foos"), XmlArrayItem(typeof(Foo), ElementName = "foo")]
Under .NET 3.5 SP1 (Specificly SP1) you can use the Serializers from WCF to deserialize objects without specificly marking the class up with DataContract or Serializable attributes.
Almost any class should be able to be deserialized this way - as long as the Property names match the element names.
If you're getting deserializer errors - then it's possibly because of some misnamed property or an incorrect type. To check the input that the Serializer is looking for, you can populate an object once, and then serialize it down to XML to compare.
I wrote myself a helper class for using this a while back.
The way to use the helper is:
string serialized = "some xml";
MyType foo = Helpers.Deserialize<MyType>(serialized, SerializerType.Xml);
The actual helper class:
using System.IO;
using System.Runtime.Serialization; // System.Runtime.Serialization.dll (.NET 3.0)
using System.Runtime.Serialization.Json; // System.ServiceModel.Web.dll (.NET 3.5)
using System.Text;
namespace Serialization
{
public static class Helpers
{
/// <summary>
/// Declare the Serializer Type you want to use.
/// </summary>
public enum SerializerType
{
Xml, // Use DataContractSerializer
Json // Use DataContractJsonSerializer
}
public static T Deserialize<T>(string SerializedString, SerializerType UseSerializer)
{
// Get a Stream representation of the string.
using (Stream s = new MemoryStream(UTF8Encoding.UTF8.GetBytes(SerializedString)))
{
T item;
switch (UseSerializer)
{
case SerializerType.Json:
// Declare Serializer with the Type we're dealing with.
var serJson = new DataContractJsonSerializer(typeof(T));
// Read(Deserialize) with Serializer and cast
item = (T)serJson.ReadObject(s);
break;
case SerializerType.Xml:
default:
var serXml = new DataContractSerializer(typeof(T));
item = (T)serXml.ReadObject(s);
break;
}
return item;
}
}
public static string Serialize<T>(T ObjectToSerialize, SerializerType UseSerializer)
{
using (MemoryStream serialiserStream = new MemoryStream())
{
string serialisedString = null;
switch (UseSerializer)
{
case SerializerType.Json:
// init the Serializer with the Type to Serialize
DataContractJsonSerializer serJson = new DataContractJsonSerializer(typeof(T));
// The serializer fills the Stream with the Object's Serialized Representation.
serJson.WriteObject(serialiserStream, ObjectToSerialize);
break;
case SerializerType.Xml:
default:
DataContractSerializer serXml = new DataContractSerializer(typeof(T));
serXml.WriteObject(serialiserStream, ObjectToSerialize);
break;
}
// Rewind the stream to the start so we can now read it.
serialiserStream.Position = 0;
using (StreamReader sr = new StreamReader(serialiserStream))
{
// Use the StreamReader to get the serialized text out
serialisedString = sr.ReadToEnd();
sr.Close();
}
return serialisedString;
}
}
}
}
There are 2 main techniques for (de)serializing objects:
Implement an interface together with its Serialize() and Deserialize() methods for each class you want to (de)serialize - fast but requires a lot of maintenance.
Use a reflection based serizlier/deserializer that analizes the public fields and properties in your classes - slower but does not require maintaining (de)serialize() methods in each class.
Personally, in many cases, I prefer the 2nd technique.
.NET's built in XmlSerializer supports the 2nd technique, but has many limitations:
1 . Multi-deminsional arrays.
2 . Deserializing objects of unexpected types:
public MyClass
{
public IMyInterface MyProperty1
{
get;
set;
}
public MyBaseType MyProperty2
{
get;
set;
}
}
The types of the actual objects in MyProperty1, MyProperty2 is unknown during deserialization.
3 . (De)serializing complex collections.
4 . No good way to handle case where fields/properties were added/remove to/from class between serialization and deserialization.
5 . No support for serializing graphs with cycles.
The solution I came up with was to write a custom reflection based serializer/deserializer,
at the time I could not find any existing serializer, so I wrote a new one from scratch.
I can not publish it, since it is proprietary, however I noticed that afterwards simular serializers were published:
http://www.codeproject.com/KB/XML/GR_CustomXmlSerializer.aspx
XML Serialization and Inherited Types
http://www.codeproject.com/KB/XML/deepserializer.aspx
!THIS IS THE BEST SOLUTION I'VE FOUND!
OK, sorry for the answer-spam here, people, but I've found an even more elegant way of doing this that avoids the need for ItemList to have its items accessed using an 'Items' property; make the ItemList a List itself! This way, you just directly access ItemList as a list. Here's the amended example program:
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
namespace XmlTester
{
public class Program {
static void Main(string[] args) {
Console.WriteLine("XML tester...");
// Valid XML for an ItemList of Person's
XmlSerializer ser = new XmlSerializer(typeof(ItemList<Person>));
string xmlIn =
#"<ItemList xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"">
<PersonBilingual>
<FullName>John Smith</FullName>
<Age>21</Age>
<Language>French</Language>
<SecondLanguage>German</SecondLanguage>
</PersonBilingual>
<Person>
<FullName>Joe Bloggs</FullName>
<Age>26</Age>
<Language>English</Language>
</Person>
<Person i:type=""PersonBilingual"">
<FullName>Jane Doe</FullName>
<Age>78</Age>
<Language>Italian</Language>
<SecondLanguage>English</SecondLanguage>
</Person>
</ItemList>";
//// Valid XML for an ItemList of Account's
//XmlSerializer ser = new XmlSerializer(typeof(ItemList<Account>));
//string xmlIn =
//#"<ItemList xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"">
// <AccountBank>
// <AcctName>Deposit account</AcctName>
// <WithCompany>Bank of Switzerland</WithCompany>
// <BalanceInEuros>300</BalanceInEuros>
// </AccountBank>
// <Account>
// <AcctName>Book buying account</AcctName>
// <WithCompany>Amazon</WithCompany>
// </Account>
// <Account i:type=""AccountBank"">
// <AcctName>Savings account</AcctName>
// <WithCompany>Bank of America</WithCompany>
// <BalanceInEuros>2500</BalanceInEuros>
// </Account>
//</ItemList>";
//// Invalid XML, as we have mixed incompatible types
//XmlSerializer ser = new XmlSerializer(typeof(ItemList<Person>));
//string xmlIn =
//#"<ItemList xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"">
// <PersonBilingual>
// <FullName>John Smith</FullName>
// <Age>21</Age>
// <Language>French</Language>
// <SecondLanguage>German</SecondLanguage>
// </PersonBilingual>
// <Account>
// <AcctName>Book buying account</AcctName>
// <WithCompany>Amazon</WithCompany>
// </Account>
// <Person i:type=""PersonBilingual"">
// <FullName>Jane Doe</FullName>
// <Age>78</Age>
// <Language>Italian</Language>
// <SecondLanguage>English</SecondLanguage>
// </Person>
//</ItemList>";
// Deserialize...
ItemList<Person> result;
using (var reader = new StringReader(xmlIn)) {
result = (ItemList<Person>)ser.Deserialize(reader);
}
Console.WriteLine("Break here and check 'result' in Quickwatch...");
Console.ReadKey();
// Serialize...
StringBuilder xmlOut = new StringBuilder();
ser.Serialize(new StringWriter(xmlOut), result);
Console.WriteLine("Break here and check 'xmlOut' in Quickwatch...");
Console.ReadKey();
}
}
[XmlRoot(ElementName = "ItemList", IsNullable = false)]
[XmlInclude(typeof(Person))]
[XmlInclude(typeof(PersonBilingual))]
[XmlInclude(typeof(Account))]
[XmlInclude(typeof(AccountBank))]
public class ItemList<T> : List<T>, IXmlSerializable {
#region Private vars
/// <summary>
/// The class that will store our serializers for the various classes that may be (de)serialized, given
/// the type of this ItemList (ie. the type itself, as well as any type that extends the type)
/// </summary>
private class Map : Dictionary<string, XmlSerializer> { public Map() : base(StringComparer.Ordinal) { } }
#endregion
#region Private methods
/// <summary>
/// Creates a 'schema' for this ItemList, using its type, and the XmlIncludeAttribute types that are
/// associated with it. For each XmlIncludeAttribute, if it can be assigned to this ItemList's type (so
/// it's either the same type as this ItemList's type or a type that extends this ItemList's type), adds
/// the XmlSerializer for that XmlIncludeAttribute's type to our 'schema' collection, allowing a node
/// corresponding to that type to be (de)serialized by this ItemList.
/// </summary>
/// <returns>The 'schema' containing the XmlSerializer's available for this ItemList to use during (de)serialization.</returns>
private Map loadSchema() {
Map map = new Map();
foreach (XmlIncludeAttribute inc in typeof(ItemList<T>).GetCustomAttributes(typeof(XmlIncludeAttribute), true)) {
Type t = inc.Type;
if (typeof(T).IsAssignableFrom(t)) { map.Add(xmlTypeName(t), new XmlSerializer(t)); }
}
return map;
}
/// <summary>
/// As the XML type name can be different to our internal class name for that XML type, we need to be able
/// to expect an XML element name that is different to our internal class name for that XML type. Hence,
/// our 'schema' map will contain XmlSerializer's whose keys are based on the XML type name, NOT our
/// internal class name for that XML type. This method returns the XML type name given our internal
/// class we're using to (de)serialize that XML type. If no XML TypeName is specified in our internal
/// class's XmlTypeAttribute, we assume an XML type name identical to the internal class name.
/// </summary>
/// <param name="t">Our internal class used to (de)serialize an XML type.</param>
/// <returns>The XML type name corresponding to the given internal class.</returns>
private string xmlTypeName(Type t) {
string typeName = t.Name;
foreach (XmlTypeAttribute ta in t.GetCustomAttributes(typeof(XmlTypeAttribute), true)) {
if (!string.IsNullOrEmpty(ta.TypeName)) { typeName = ta.TypeName; }
}
return typeName;
}
#endregion
#region IXmlSerializable Members
/// <summary>
/// Reserved and should not be used.
/// </summary>
/// <returns>Must return null.</returns>
public XmlSchema GetSchema() {
return null;
}
/// <summary>
/// Generates a list of type T objects from their XML representation; stores them in this ItemList.
/// </summary>
/// <param name="reader">The System.Xml.XmlReader stream from which the objects are deserialized.</param>
public void ReadXml(XmlReader reader) {
Map map = loadSchema();
int depth = reader.Depth;
List<T> items = new List<T>();
if (!reader.IsEmptyElement && reader.Read()) {
// While the reader is at a greater depth that the initial depth, ie. at one of the elements
// inside the list wrapper, the initial depth being that of the list wrapper <ItemList>...
while (reader.Depth > depth) {
try { items.Add((T)map[reader.LocalName].Deserialize(reader)); }
catch (InvalidOperationException iopEx) {
if (
iopEx.InnerException != null &&
iopEx.InnerException.Message.StartsWith("The specified type was not recognized")
) { throw new InvalidOperationException("Couldn't deserialize node '" + reader.LocalName + "' because although its element node is a valid type, its attribute-specified type was not recognized. Perhaps it needs adding to the ItemList using XmlIncludeAttribute?", iopEx); }
}
catch (KeyNotFoundException knfEx) {
throw new InvalidOperationException("Couldn't deserialize node '" + reader.LocalName + "' because its element node was not recognized as a valid type. Perhaps it needs adding to the ItemList using XmlIncludeAttribute?", knfEx);
}
catch (Exception ex) {
throw ex;
}
}
}
this.AddRange(items);
}
/// <summary>
/// Converts a list of type T objects into their XML representation; writes them to the specified writer.
/// </summary>
/// <param name="writer">The System.Xml.XmlWriter stream to which the objects are serialized.</param>
public void WriteXml(XmlWriter writer) {
Map map = loadSchema();
foreach (T item in this) {
map[xmlTypeName(item.GetType())].Serialize(writer, item);
}
}
#endregion
}
/// <summary>
/// A regular person.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "Person", Namespace = "")]
[XmlInclude(typeof(PersonBilingual))]
public class Person {
public string FullName { get; set; }
public int Age { get; set; }
public string Language { get; set; }
}
/// <summary>
/// A person who can speak a second language.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "PersonBilingual", Namespace = "")]
public class PersonBilingual : Person {
public string SecondLanguage { get; set; }
}
/// <summary>
/// Some kind of account.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "Account", Namespace = "")]
[XmlInclude(typeof(AccountBank))]
public class Account {
public string AcctName { get; set; }
public string WithCompany { get; set; }
}
/// <summary>
/// A bank account.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "AccountBank", Namespace = "")]
public class AccountBank : Account {
public int BalanceInEuros { get; set; }
}
}
UPDATE: Please see the answer beginning !THIS IS THE BEST SOLUTION I'VE FOUND! - it's a better solution than this one.
...
Heavily inspired by csharptest.net's comment, I've created a class that pretty much does the job I wanted. :-) You access the deserialized items by checking ItemList.Items, and serialize stuff by inserting the items into ItemList.Items and then serializing it using an appropriate XmlSerializer. The only slight annoyance is that you must ensure that the ItemList class is tagged with an XmlIncludeAttribute for every class type that may need to be (de)serialized, or the XmlSerializer won't be able to deal with it.
Here's the example program, containing the generic ItemList class:
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
namespace XmlTester
{
public class Program {
static void Main(string[] args) {
Console.WriteLine("XML tester...");
// Valid XML for an ItemList of Person's
XmlSerializer ser = new XmlSerializer(typeof(ItemList<Person>));
string xmlIn =
#"<ItemList xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"">
<PersonBilingual>
<FullName>John Smith</FullName>
<Age>21</Age>
<Language>French</Language>
<SecondLanguage>German</SecondLanguage>
</PersonBilingual>
<Person>
<FullName>Joe Bloggs</FullName>
<Age>26</Age>
<Language>English</Language>
</Person>
<Person i:type=""PersonBilingual"">
<FullName>Jane Doe</FullName>
<Age>78</Age>
<Language>Italian</Language>
<SecondLanguage>English</SecondLanguage>
</Person>
</ItemList>";
//// Valid XML for an ItemList of Account's
//XmlSerializer ser = new XmlSerializer(typeof(ItemList<Account>));
//string xmlIn =
//#"<ItemList xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"">
// <AccountBank>
// <AcctName>Deposit account</AcctName>
// <WithCompany>Bank of Switzerland</WithCompany>
// <BalanceInEuros>300</BalanceInEuros>
// </AccountBank>
// <Account>
// <AcctName>Book buying account</AcctName>
// <WithCompany>Amazon</WithCompany>
// </Account>
// <Account i:type=""AccountBank"">
// <AcctName>Savings account</AcctName>
// <WithCompany>Bank of America</WithCompany>
// <BalanceInEuros>2500</BalanceInEuros>
// </Account>
//</ItemList>";
//// Invalid XML, as we have mixed incompatible types
//XmlSerializer ser = new XmlSerializer(typeof(ItemList<Person>));
//string xmlIn =
//#"<ItemList xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"">
// <PersonBilingual>
// <FullName>John Smith</FullName>
// <Age>21</Age>
// <Language>French</Language>
// <SecondLanguage>German</SecondLanguage>
// </PersonBilingual>
// <Account>
// <AcctName>Book buying account</AcctName>
// <WithCompany>Amazon</WithCompany>
// </Account>
// <Person i:type=""PersonBilingual"">
// <FullName>Jane Doe</FullName>
// <Age>78</Age>
// <Language>Italian</Language>
// <SecondLanguage>English</SecondLanguage>
// </Person>
//</ItemList>";
// Deserialize...
ItemList<Person> result;
using (var reader = new StringReader(xmlIn)) {
result = (ItemList<Person>)ser.Deserialize(reader);
}
Console.WriteLine("Break here and check 'result' in Quickwatch...");
Console.ReadKey();
// Serialize...
StringBuilder xmlOut = new StringBuilder();
ser.Serialize(new StringWriter(xmlOut), result);
Console.WriteLine("Break here and check 'xmlOut' in Quickwatch...");
Console.ReadKey();
}
}
[XmlRoot(ElementName = "ItemList", IsNullable = false)]
[XmlInclude(typeof(Person))]
[XmlInclude(typeof(PersonBilingual))]
[XmlInclude(typeof(Account))]
[XmlInclude(typeof(AccountBank))]
public class ItemList<T> : IXmlSerializable {
#region Private vars
/// <summary>
/// The class that will store our serializers for the various classes that may be (de)serialized, given
/// the type of this ItemList (ie. the type itself, as well as any type that extends the type)
/// </summary>
private class Map : Dictionary<string, XmlSerializer> { public Map() : base(StringComparer.Ordinal) { } }
#endregion
#region Private methods
/// <summary>
/// Creates a 'schema' for this ItemList, using its type, and the XmlIncludeAttribute types that are
/// associated with it. For each XmlIncludeAttribute, if it can be assigned to this ItemList's type (so
/// it's either the same type as this ItemList's type or a type that extends this ItemList's type), adds
/// the XmlSerializer for that XmlIncludeAttribute's type to our 'schema' collection, allowing a node
/// corresponding to that type to be (de)serialized by this ItemList.
/// </summary>
/// <returns>The 'schema' containing the XmlSerializer's available for this ItemList to use during (de)serialization.</returns>
private Map loadSchema() {
Map map = new Map();
foreach (XmlIncludeAttribute inc in typeof(ItemList<T>).GetCustomAttributes(typeof(XmlIncludeAttribute), true)) {
Type t = inc.Type;
if (typeof(T).IsAssignableFrom(t)) { map.Add(xmlTypeName(t), new XmlSerializer(t)); }
}
return map;
}
/// <summary>
/// As the XML type name can be different to our internal class name for that XML type, we need to be able
/// to expect an XML element name that is different to our internal class name for that XML type. Hence,
/// our 'schema' map will contain XmlSerializer's whose keys are based on the XML type name, NOT our
/// internal class name for that XML type. This method returns the XML type name given our internal
/// class we're using to (de)serialize that XML type. If no XML TypeName is specified in our internal
/// class's XmlTypeAttribute, we assume an XML type name identical to the internal class name.
/// </summary>
/// <param name="t">Our internal class used to (de)serialize an XML type.</param>
/// <returns>The XML type name corresponding to the given internal class.</returns>
private string xmlTypeName(Type t) {
string typeName = t.Name;
foreach (XmlTypeAttribute ta in t.GetCustomAttributes(typeof(XmlTypeAttribute), true)) {
if (!string.IsNullOrEmpty(ta.TypeName)) { typeName = ta.TypeName; }
}
return typeName;
}
#endregion
#region IXmlSerializable Members
/// <summary>
/// Reserved and should not be used.
/// </summary>
/// <returns>Must return null.</returns>
public XmlSchema GetSchema() {
return null;
}
/// <summary>
/// Generates a list of type T objects from their XML representation; stores them in this.Items.
/// </summary>
/// <param name="reader">The System.Xml.XmlReader stream from which the objects are deserialized.</param>
public void ReadXml(XmlReader reader) {
Map map = loadSchema();
int depth = reader.Depth;
List<T> items = new List<T>();
if (!reader.IsEmptyElement && reader.Read()) {
// While the reader is at a greater depth that the initial depth, ie. at one of the elements
// inside the list wrapper, the initial depth being that of the list wrapper <ItemList>...
while (reader.Depth > depth) {
try { items.Add((T)map[reader.LocalName].Deserialize(reader)); }
catch (InvalidOperationException iopEx) {
if (
iopEx.InnerException != null &&
iopEx.InnerException.Message.StartsWith("The specified type was not recognized")
) { throw new InvalidOperationException("Couldn't deserialize node '" + reader.LocalName + "' because although its element node is a valid type, its attribute-specified type was not recognized. Perhaps it needs adding to the ItemList using XmlIncludeAttribute?", iopEx); }
}
catch (KeyNotFoundException knfEx) {
throw new InvalidOperationException("Couldn't deserialize node '" + reader.LocalName + "' because its element node was not recognized as a valid type. Perhaps it needs adding to the ItemList using XmlIncludeAttribute?", knfEx);
}
catch (Exception ex) {
throw ex;
}
}
}
this.Items = items;
}
/// <summary>
/// Converts a list of type T objects into their XML representation; writes them to the specified writer.
/// </summary>
/// <param name="writer">The System.Xml.XmlWriter stream to which the objects are serialized.</param>
public void WriteXml(XmlWriter writer) {
Map map = loadSchema();
foreach (T item in this.Items) {
map[xmlTypeName(item.GetType())].Serialize(writer, item);
}
}
#endregion
#region Public properties
public List<T> Items { get; set; }
#endregion
}
/// <summary>
/// A regular person.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "Person", Namespace = "")]
[XmlInclude(typeof(PersonBilingual))]
public class Person {
public string FullName { get; set; }
public int Age { get; set; }
public string Language { get; set; }
}
/// <summary>
/// A person who can speak a second language.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "PersonBilingual", Namespace = "")]
public class PersonBilingual : Person {
public string SecondLanguage { get; set; }
}
/// <summary>
/// Some kind of account.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "Account", Namespace = "")]
[XmlInclude(typeof(AccountBank))]
public class Account {
public string AcctName { get; set; }
public string WithCompany { get; set; }
}
/// <summary>
/// A bank account.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "AccountBank", Namespace = "")]
public class AccountBank : Account {
public int BalanceInEuros { get; set; }
}
}
Thanks everyone for your help!

Categories

Resources