Search code examples
c#timezonexmlserializer

XmlSerializer does not account for local timezone when deserializing property annotated with XmlElement with DataType time


When I deserialize a time string, using XmlSerializer.Deserialize, I expect it to take my local timezone into account so that a time string in the format

00:00:00.0000000+01:00

was parsed as 00:00, because I am in the timezone GMT+1.

Did I get that wrong?

Here is the code I am running to test xml deserialization:

using System;
using System.IO;
using System.Xml.Serialization;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Testing
{
    [TestClass]
    public class FooTest
    {
        [TestMethod]
        public void Test()
        {
            var serializer = new XmlSerializer(typeof(Foo),
                new XmlRootAttribute("Foo"));

            var xml = "<Foo><TheTime>00:00:00.0000000+01:00</TheTime></Foo>";

            var stream = new MemoryStream();
            var writer = new StreamWriter(stream);
            writer.Write(xml);
            writer.Flush();
            stream.Position = 0;

            var f = (Foo) serializer.Deserialize(stream);

            Assert.AreEqual("00:00", f.TheTime.ToShortTimeString()); // actual: 01:00
        }

        [Serializable]
        public class Foo
        {
            [XmlElement(DataType = "time")]
            public DateTime TheTime { get; set; }
        }
    }
}

Solution

  • Unfortunately, there is no built-in type that you can deserialize a xs:time value into when it includes an offset (which is optional in the XSD spec).

    Instead, you'll need to define a custom type and implement the appropriate interfaces for custom serialization and deserialization. Below is a minimal TimeOffset struct that will do just that.

    [XmlSchemaProvider("GetSchema")]
    public struct TimeOffset : IXmlSerializable
    {
        public DateTime Time { get; set; }
        public TimeSpan Offset { get; set; }
    
        public static XmlQualifiedName GetSchema(object xs)
        {
            return new XmlQualifiedName("time", "http://www.w3.org/2001/XMLSchema");
        }
    
        XmlSchema IXmlSerializable.GetSchema()
        {
            // this method isn't actually used, but is required to be implemented
            return null;
        }
    
        void IXmlSerializable.ReadXml(XmlReader reader)
        {
            var s = reader.NodeType == XmlNodeType.Element
                ? reader.ReadElementContentAsString()
                : reader.ReadContentAsString();
    
            if (!DateTimeOffset.TryParseExact(s, "HH:mm:ss.FFFFFFFzzz",
                CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto))
            {
                throw new FormatException("Invalid time format.");
            }
    
            this.Time = dto.DateTime;
            this.Offset = dto.Offset;
        }
    
        void IXmlSerializable.WriteXml(XmlWriter writer)
        {
            var dto = new DateTimeOffset(this.Time, this.Offset);
            writer.WriteString(dto.ToString("HH:mm:ss.FFFFFFFzzz", CultureInfo.InvariantCulture));
        }
    
        public string ToShortTimeString()
        {
            return this.Time.ToString("HH:mm", CultureInfo.InvariantCulture);
        }
    }
    

    With this defined, you can now change the type of Foo.TheTime in your code to be a TimeOffset and your test will pass. You can also remove the DataType="time" in the attribute, as it's declared in the object itself via the GetSchema method.