XML deserialization crashes on decimal parse due to formatting
Asked Answered
O

3

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"?

Oulu answered 3/7, 2013 at 0:33 Comment(2)
From here it sounds like the XmlSerializer is using this W3C schema 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.Solidarity
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.Solidarity
M
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.

Mountebank answered 3/7, 2013 at 0:40 Comment(6)
Even so, the XML returned from my queries contains doubles with commas.Oulu
@Softnux, I reckon you found one possible solution.Mountebank
+1. @Oulu "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.Groome
Yes, it's from a third party and I hove no intention of serializing it again.Oulu
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.Grishilde
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 workCheboksary
S
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.

Selfexamination answered 3/7, 2013 at 1:46 Comment(0)
L
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;
    }
Letaletch answered 3/7, 2013 at 1:8 Comment(1)
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 appropriateGrishilde

© 2022 - 2024 — McMap. All rights reserved.