Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Change properties to camelCase when serializing to XML in C#

I have multiple DTO classes that are (de)serialized from and to XML. I want to use the C# convention of PascalCase for properties, but I want them to appear as camelCase in the XML.

Example:

[XmlElement("config")]
public ConfigType Config { get; set; }

Here the property is Config but appears as config in the XML.

I feel that using a [XmlAttribute] for each property is wasteful, considering they are always the same as the property name, only the first letter is not capitalized. Also if I change the property name in the future I must remember to change the [XmlAttribute] or they will become out of sync.

Is it possible to have a class-wide attribute that says "use camel case for the properties even though they are pascal case", or even better, a setting to XmlSerializer?

like image 218
sashoalm Avatar asked Jun 14 '17 11:06

sashoalm


People also ask

What is the correct way of using XML serialization?

As with the CreatePo method, you must first construct an XmlSerializer, passing the type of class to be deserialized to the constructor. Also, a FileStream is required to read the XML document. To deserialize the objects, call the Deserialize method with the FileStream as an argument.

What is serializing XML?

XML serialization is the process of converting XML data from its representation in the XQuery and XPath data model, which is the hierarchical format it has in a Db2® database, to the serialized string format that it has in an application.

What occasions the XML serialization throws an exception?

XmlSerializer throws exception when the type belongs to an assembly with no location property set #24614.


2 Answers

You can create a custom XmlWriter wrapping the one provided by the framework with something like:

public class MyXmlWriter : XmlWriter
{
    private bool disposedValue;
    private XmlWriter writer; // The XmlWriter that will actually write the xml
    public override WriteState WriteState => writer.WriteState;

    public MyXmlWriter(XmlWriter writer)
    {
        this.writer = writer;
    }

    public override void WriteStartElement(string prefix, string localName, string ns)
    {
        localName = char.ToLower(localName[0]) + localName.Substring(1); // Assuming that your properties are in PascalCase we just need to lower-case the first letter.
        writer.WriteStartElement(prefix, localName, ns);
    }

    public override void WriteStartAttribute(string prefix, string localName, string ns)
    {
        // If you want to do the same with attributes you can do the same here
        writer.WriteStartAttribute(prefix, localName, ns); 
    }

    protected override void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                writer.Dispose();
                base.Dispose(disposing);
            }
            disposedValue = true;
        }
    }

    // Wrapping every other methods...
    public override void Flush()
    {
        writer.Flush();
    }

    public override string LookupPrefix(string ns)
    {
        return writer.LookupPrefix(ns);
    }

    public override void WriteBase64(byte[] buffer, int index, int count)
    {
        writer.WriteBase64(buffer, index, count);
    }

    public override void WriteCData(string text)
    {
        writer.WriteCData(text);
    }

    public override void WriteCharEntity(char ch)
    {
        writer.WriteCharEntity(ch);
    }

    public override void WriteChars(char[] buffer, int index, int count)
    {
        writer.WriteChars(buffer, index, count);
    }

    public override void WriteComment(string text)
    {
        writer.WriteComment(text);
    }

    public override void WriteDocType(string name, string pubid, string sysid, string subset)
    {
        writer.WriteDocType(name, pubid, sysid, subset);
    }

    public override void WriteEndAttribute()
    {
        writer.WriteEndAttribute();
    }

    public override void WriteEndDocument()
    {
        writer.WriteEndDocument();
    }

    public override void WriteEndElement()
    {
        writer.WriteEndElement();
    }

    public override void WriteEntityRef(string name)
    {
        writer.WriteEntityRef(name);
    }

    public override void WriteFullEndElement()
    {
        writer.WriteFullEndElement();
    }

    public override void WriteProcessingInstruction(string name, string text)
    {
        writer.WriteProcessingInstruction(name, text);
    }

    public override void WriteRaw(char[] buffer, int index, int count)
    {
        writer.WriteRaw(buffer, index, count);
    }

    public override void WriteRaw(string data)
    {
        writer.WriteRaw(data);
    }

    public override void WriteStartDocument()
    {
        writer.WriteStartDocument();
    }

    public override void WriteStartDocument(bool standalone)
    {
        writer.WriteStartDocument(standalone);
    }

    public override void WriteString(string text)
    {
        writer.WriteString(text);
    }

    public override void WriteSurrogateCharEntity(char lowChar, char highChar)
    {
        writer.WriteSurrogateCharEntity(lowChar, highChar);
    }

    public override void WriteWhitespace(string ws)
    {
        writer.WriteWhitespace(ws);
    }
}

And then use it like:

public class CustomClassOne
{
    public string MyCustomName { get; set; }
    public CustomClassTwo MyOtherProperty { get; set; }
    public CustomClassTwo[] MyArray { get; set; }
}
public class CustomClassTwo
{
    public string MyOtherCustomName { get; set; }
}
.
.
.
static void Main(string[] args)
{
    var myObj = new CustomClassOne()
    {
        MyCustomName = "MYNAME",
        MyOtherProperty = new CustomClassTwo()
        {
            MyOtherCustomName = "MyOtherName"
        },
        MyArray = new CustomClassTwo[]
        {
            new CustomClassTwo(){MyOtherCustomName = "Elem1"},
            new CustomClassTwo(){MyOtherCustomName = "Elem2"}
        }
    };
    var sb = new StringBuilder();
    var serializer = new XmlSerializer(typeof(CustomClassOne));
    var settings = new XmlWriterSettings()
    {
        Indent = true // Indent it so we can see it better
    };
    using (var sw = new StringWriter(sb))
    using (var xw = new MyXmlWriter(XmlWriter.Create(sw, settings)))
    {
        serializer.Serialize(xw, myObj);
    }
    Console.WriteLine(sb.ToString());
}

And the output will be:

<?xml version="1.0" encoding="utf-16"?>
<customClassOne xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <myCustomName>MYNAME</myCustomName>
  <myOtherProperty>
    <myOtherCustomName>MyOtherName</myOtherCustomName>
  </myOtherProperty>
  <myArray>
    <customClassTwo>
      <myOtherCustomName>Elem1</myOtherCustomName>
    </customClassTwo>
    <customClassTwo>
      <myOtherCustomName>Elem2</myOtherCustomName>
    </customClassTwo>
  </myArray>
</customClassOne>

To be able to deserialize we can create a wrapper on XmlReader like the one made on XmlWriter:

public class MyXmlReader : XmlReader
{
    private bool disposedValue;
    private XmlReader reader;
    // The property names will be added in the XmlNameTable, so we wrap it with a simple class that lower-cases the first letter like we did previously
    private XmlNameTableWrapper nameTable;

    private class XmlNameTableWrapper : XmlNameTable
    {
        private XmlNameTable wrapped;
        // Some names that are added by default to this collection. We can skip the lower casing logic on them.
        private string[] defaultNames = new string[]
            {
                "http://www.w3.org/2001/XMLSchema","http://www.w3.org/2000/10/XMLSchema","http://www.w3.org/1999/XMLSchema","http://microsoft.com/wsdl/types/","http://www.w3.org/2001/XMLSchema-instance","http://www.w3.org/2000/10/XMLSchema-instance","http://www.w3.org/1999/XMLSchema-instance","http://schemas.xmlsoap.org/soap/encoding/","http://www.w3.org/2003/05/soap-encoding","schema","http://schemas.xmlsoap.org/wsdl/","arrayType","null","nil","type","arrayType","itemType","arraySize","Array","anyType"
            };

        public XmlNameTableWrapper(XmlNameTable wrapped)
        {
            this.wrapped = wrapped;
        }

        public override string Add(char[] array, int offset, int length)
        {
            if (array != null && array.Length > 0 && !defaultNames.Any(n => n == new string(array)))
            {
                array[0] = char.ToLower(array[0]);
            }
            return wrapped.Add(array, offset, length);
        }

        public override string Add(string array)
        {
            if (array != null && !defaultNames.Any(n => n == array))
            {
                if (array.Length < 2)
                {
                    array = array.ToLower();
                }
                else
                    array = char.ToLower(array[0]) + array.Substring(1);
            }
            return wrapped.Add(array);
        }

        public override string Get(char[] array, int offset, int length)
        {
            if (array != null && array.Length > 0 && !defaultNames.Any(n => n == new string(array)))
            {
                array[0] = char.ToLower(array[0]);
            }
            return wrapped.Get(array, offset, length);
        }

        public override string Get(string array)
        {
            if (array != null && !defaultNames.Any(n => n == array))
            {
                if (array.Length < 2)
                {
                    array = array.ToLower();
                }
                array = char.ToLower(array[0]) + array.Substring(1);
            }
            return wrapped.Get(array);
        }
    }

    public MyXmlReader(XmlReader reader)
    {
        this.reader = reader;
        nameTable = new XmlNameTableWrapper(reader.NameTable);
    }

    protected override void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                reader.Dispose();
                base.Dispose(disposing);
            }
            disposedValue = true;
        }
    }

    // Instead of returning reader.NameTable we return the wrapper that will care to populate it
    public override XmlNameTable NameTable => nameTable;

    // Everything else does not need additional logic...
    public override XmlNodeType NodeType => reader.NodeType;

    public override string LocalName => reader.LocalName;

    public override string NamespaceURI => reader.NamespaceURI;

    public override string Prefix => reader.Prefix;

    public override string Value => reader.Value;

    public override int Depth => reader.Depth;

    public override string BaseURI => reader.BaseURI;

    public override bool IsEmptyElement => reader.IsEmptyElement;

    public override int AttributeCount => reader.AttributeCount;

    public override bool EOF => reader.EOF;

    public override ReadState ReadState => reader.ReadState;

    public override string GetAttribute(string name)
    {
        return reader.GetAttribute(name);
    }

    public override string GetAttribute(string name, string namespaceURI)
    {
        return reader.GetAttribute(name, namespaceURI);
    }

    public override string GetAttribute(int i)
    {
        return reader.GetAttribute(i);
    }

    public override string LookupNamespace(string prefix)
    {
        return reader.LookupNamespace(prefix);
    }

    public override bool MoveToAttribute(string name)
    {
        return reader.MoveToAttribute(name);
    }

    public override bool MoveToAttribute(string name, string ns)
    {
        return reader.MoveToAttribute(name, ns);
    }

    public override bool MoveToElement()
    {
        return reader.MoveToElement();
    }

    public override bool MoveToFirstAttribute()
    {
        return reader.MoveToFirstAttribute();
    }

    public override bool MoveToNextAttribute()
    {
        return reader.MoveToNextAttribute();
    }

    public override bool Read()
    {
        return reader.Read();
    }

    public override bool ReadAttributeValue()
    {
        return reader.ReadAttributeValue();
    }

    public override void ResolveEntity()
    {
        reader.ResolveEntity();
    }
}

Then use it like:

var serializer = new XmlSerializer(typeof(CustomClassOne));
using (var sr = new StringReader(theXmlGeneratedBefore))
using (var xr = new MyXmlReader(XmlReader.Create(sr)))
{
    var o = serializer.Deserialize(xr);
}
like image 189
Matteo Umili Avatar answered Sep 17 '22 12:09

Matteo Umili


This is most probably not the best solution (from performance perspective for sure) but it is quite concise and neat in my opinion.

Sample data object with PascalCasing

public class ReportSummary
{
    public string Name { get; set; }
    public bool IsSuccess { get; set; }
}

Taking advantage of Newtonsoft.Json

var summary = new ReportSummary { Name = "My Awesome Report", IsSuccess = true };

var resolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() };
var settings = new JsonSerializerSettings { ContractResolver = resolver };

var json = JsonConvert.SerializeObject(summary, settings);
XNode xml = JsonConvert.DeserializeXNode(json, ToCamelCasing(nameof(ReportSummary)));
Console.WriteLine(xml.ToString());

ToCamelCasing helper for the top level node

static string ToCamelCasing(string input) 
  => char.ToLowerInvariant(input[0]) + input.Substring(1);

The output with camelCasing

<reportSummary>
  <name>My Awesome Report</name>
  <isSuccess>true</isSuccess>
</reportSummary>
like image 40
Peter Csala Avatar answered Sep 18 '22 12:09

Peter Csala