7

I get a System.FormatException thrown when i try to parse XML into an object. As far as I can tell, it's due to the culture used in System.Xml.Serialization.XmlSerializer.Deserialize, wich expects a dot as the decimal character, but the xml contains a comma.

The object looks as follows:


public sealed class Transaction
{
    [XmlElement("transactionDate")]
    public DateTime TransactionDate { get; set; }

    [XmlElement("transactionAmount")]
    public decimal Amount { get; set; }

    [XmlElement("transactionDescription")]
    public string Description { get; set; }

    [XmlElement("transactionType")]
    public int Type { get; set; }

    public static Transaction FromXmlString(string xmlString)
    {
        var reader = new StringReader(xmlString);
        var serializer = new XmlSerializer(typeof(Transaction));
        var instance = (Transaction) serializer.Deserialize(reader);

        return instance;
    }
}

The xml:


<transaction>
    <transactionDate> 2013-07-02 <transactionDate>
    <transactionAmount>-459,00</transactionAmount>
    <transactionDescription>description</transactionDescription>
    <transactionType>1</transactionType>
</transaction>

I've made it work by introducing a second property that parses the first using my own culture:


namespace MyNamespace
{
    [XmlRoot("transaction"), XmlType("Transaction")]
    public sealed class Transaction
    {
        [XmlElement("transactionDate")]
        public DateTime TransactionDate { get; set; }

        [XmlElement("transactionAmount")]
        public string Amount { get; set; }

        public decimal AmountAsDecimal {
            get
            {
                decimal value;
                Decimal.TryParse(Amount, NumberStyles.Any, CultureInfo.CreateSpecificCulture("sv-SE"), out value);
                return value;
            }
        }

        [XmlElement("transactionDescription")]
        public string Description { get; set; }

        [XmlElement("transactionType")]
        public int Type { get; set; }

        public static Transaction FromXmlString(string xmlString)
        {
            var reader = new StringReader(xmlString);
            var serializer = new XmlSerializer(typeof(Transaction));
            var instance = (Transaction) serializer.Deserialize(reader);

            return instance;
        }
    }
}


which exposes an extra property that i don't want there.

So my question is: is there another way to do this, without iterating over each element and parsing/assigning it to the object "manually"?

Kirill Polishchuk
  • 54,804
  • 11
  • 122
  • 125
Softnux
  • 2,440
  • 4
  • 20
  • 21
  • 1
    From [here](http://forums.asp.net/t/1365779.aspx) it sounds like the XmlSerializer is using [this W3C schema](http://www.w3.org/TR/xmlschema-2/) and is supposed to be relatively independent of culture to avoid serialization/deserialization issues across machines/cultures. I would _guess_ that what you're doing now is probably the best way (maybe adorn `AmountAsDecimal` with `[XmlIgnore]` just so it's a bit more obvious). Regardless, so long as your serialized objects are purely data-transfer objects and abstracted from your application/business logic, then it shouldn't hurt too much I hope. – Chris Sinclair Jul 03 '13 at 00:40
  • But on second read, you may want to flip which property parses and which serializes. Expose `public decimal Amount` as the value that you would get/set normally in your _code_ API but have it as `[XmlIgnore]`. Then have a `public string SerializedAmount` property whose get/set implementations would format/parse your specialized culture. At least this way the API you use to _write_ the `Transaction` object doesn't have to think how to format the string; it just writes a `decimal` value. If the way you write it (you change the `SerializedAmount` property for example) then your code doesn't care. – Chris Sinclair Jul 03 '13 at 00:45

3 Answers3

6

XML serializer uses a standardized Number and DateTime format, the standard is defined in the W3C schema datatype specification http://www.w3.org/TR/xmlschema-2/.

Don't expect XmlSerializer to pay attention to the thread's CultureInfo, it intentionally uses a standardized format to ensure you can serialize/deserialize independent of the culture/locale.

Kirill Polishchuk
  • 54,804
  • 11
  • 122
  • 125
  • Even so, the XML returned from my queries contains doubles with commas. – Softnux Jul 03 '13 at 00:43
  • 1
    @Softnux, I reckon you found one possible solution. – Kirill Polishchuk Jul 03 '13 at 00:58
  • 1
    +1. @Softnux "my queries" as in "your creates this XML" or "queries you execute against third party service"? If former I strongly recommend to use proper XML format for numbers/datetime values. Will be much easier in long run. – Alexei Levenkov Jul 03 '13 at 01:50
  • 1
    Yes, it's from a third party and I hove no intention of serializing it again. – Softnux Jul 03 '13 at 09:52
  • My issue was concerned with XMLs created before we implemented serialization. (Yes a bunch of code was written for XML IO at the textual level). To maintain this compatibility, I serialized all data to strings and handled the format issue on my own. Ugly and kludged but it worked. I encapsulated the formatting issues with a single bridge class bridging the applications internal state with the state obtained through the XML IO. – Steve Feb 29 '16 at 21:35
  • In my case XmlSerializer format number with comma as the server is set this way.. other server is set to en-GB region and cannot parse this output.. i have tried to put CultureInfo ci = new CultureInfo("en-GB"); System.Threading.Thread.CurrentThread.CurrentCulture = ci; System.Threading.Thread.CurrentThread.CurrentUICulture = ci; in front of serialization, but it does not work – Scholtz Aug 06 '18 at 09:43
5

What you can do instead is have a property that will be used to serialize/deserialize the decimal.

See: Partially deserialize XML to Object

[XmlType("transaction")]
public sealed class Transaction
{
    [XmlElement("transactionDate")]
    public DateTime TransactionDate { get; set; }

    [XmlIgnore]
    public decimal Amount { get; set; }

    [XmlElement("transactionAmount")]
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    public string AmountSerialized
    {
        get
        {
            return Amount.ToString(CultureInfo.CreateSpecificCulture("sv-SE"));
        }
        set
        {
            decimal amount;
            Decimal.TryParse(value, NumberStyles.Any, CultureInfo.CreateSpecificCulture("sv-SE"), out amount);
            Amount = amount;
        }
    }

    [XmlElement("transactionDescription")]
    public string Description { get; set; }

    [XmlElement("transactionType")]
    public int Type { get; set; }

    public static Transaction FromXmlString(string xmlString)
    {
        var reader = new StringReader(xmlString);
        var serializer = new XmlSerializer(typeof(Transaction));
        var instance = (Transaction) serializer.Deserialize(reader);

        return instance;
    }
}

This way you can get/set the Amount without needing to worry about how it is serialized. Since this is a DTO you can create another class without the AmountSerialized as your domain object (and use something like AutoMapper to make conversion painless).

Usage:

var data = @"<transaction>
                <transactionDate>2013-07-02</transactionDate>
                <transactionAmount>-459,00</transactionAmount>
                <transactionDescription>description</transactionDescription>
                <transactionType>1</transactionType>
            </transaction>";

var serializer = new XmlSerializer(typeof(Transaction));

using(var stream = new StringReader(data))
using(var reader = XmlReader.Create(stream))
{
     Console.Write(serializer.Deserialize(reader));
}

Also there was a typo in the ending tag for transactionDate.

Community
  • 1
  • 1
Dustin Kingen
  • 20,677
  • 7
  • 52
  • 92
3

If you know the culture that the XML was generated in, one easy solution is to switch the current thread's culture to that culture prior to deserializing.

    System.Globalization.CultureInfo oCurrentCulture = null;
    try
    {
        // Save the current culture
        oCurrentCulture = System.Threading.Thread.CurrentThread.CurrentCulture;
        System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("de-DE");

        // Do your work
    }
    finally
    {
                    // Restore the saved culture
        System.Threading.Thread.CurrentThread.CurrentCulture = oCurrentCulture;
    }
competent_tech
  • 44,465
  • 11
  • 90
  • 113
  • I have found best practice is have the least amount of code within a try (or using). Since saving off the culture isn't necessary to be within the try saving the thread's culture before hand is appropriate – Steve Feb 29 '16 at 21:24