bixarrio bixarrio - 24 days ago 16
C# Question

Newtonsoft.Json JsonConvert to XmlDocument date formatting inconsistent when elements have attributes

The

Newtonsoft.Json
libraries'
JsonConvert.DeserializeXmlNode
gives inconsistent datetime results when elements have attributes on them.

Here is a small example that demonstrates the issue

public void Main(string[] args)
{
var now = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss");
var xml = $"<timestamp>{now}</timestamp>";
Debug.WriteLine(xml);
// <timestamp>2016-11-14T14:51:32</timestamp>
var json = XmlToJson(xml);
Debug.WriteLine(json);
// {"timestamp":"2016-11-14T14:51:32"}
var good = JsonToXml(json);
Debug.WriteLine(good);
// <?xml version="1.0" encoding="utf-8"?><timestamp>2016-11-14T14:51:32</timestamp>

var xml_with_attr = $"<timestamp id=\"1\">{now}</timestamp>";
Debug.WriteLine(xml_with_attr);
// <timestamp id="1">2016-11-14T14:51:32</timestamp>
var json_with_attr = XmlToJson(xml_with_attr);
Debug.WriteLine(json_with_attr);
// {"timestamp":{"@id":"1","#text":"2016-11-14T14:51:32"}}
var bad = JsonToXml(json_with_attr);
Debug.WriteLine(bad);
// <?xml version="1.0" encoding="utf-8"?><timestamp id="1">2016-11-14 2:51:32 PM</timestamp>
}

private string XmlToJson(string xml)
{

var doc = new XmlDocument();
doc.LoadXml(xml);
var json = JsonConvert.SerializeXmlNode(doc);
return json;
}
private string JsonToXml(string json)
{
var doc = JsonConvert.DeserializeXmlNode(json);
var xml = string.Empty;
var settings = new XmlWriterSettings
{
CloseOutput = true,
Encoding = Encoding.UTF8,
};
using (var ms = new MemoryStream())
using (var xw = XmlWriter.Create(ms, settings))
{
doc.WriteTo(xw);
xw.Flush();
xml = settings.Encoding.GetString(ms.ToArray());
}
return xml;
}


As you can see, the
bad
date is not in the same format as all the previous results. This is unfortunately causing the xml to fail schema validation once it gets verified against the schema.

I know about the
DateTimeConverter
stuff, but converting to and from a XmlDocument does not give me that option.

I can also - unfortunately - not do the
JsonConvert
on the schema generated class because I have no idea what it might be at the time of execution.

Does anyone know how I can get the same format back when the element has an attribute?

Thanks

dbc dbc
Answer

This seems to be a bug in Json.NET's XmlNodeConverter. You might want to report an issue.

The workaround is to disable date parsing when converting from JSON to XML. Please note that this works reliably only as long as all dates and times in the JSON are already in ISO 8601 format. Since that seems to be true in your test case, you should be OK:

private static string JsonToXml(string json)
{
    var settings = new JsonSerializerSettings
    {
        Converters = { new Newtonsoft.Json.Converters.XmlNodeConverter() },
        DateParseHandling = DateParseHandling.None,
    };

    var doc = JsonConvert.DeserializeObject<XmlDocument>(json, settings);
    var xmlSettings = new XmlWriterSettings
    {
        CloseOutput = true,
        Encoding = Encoding.UTF8,
    };

    string xml;
    using (var ms = new MemoryStream())
    using (var xw = XmlWriter.Create(ms, xmlSettings))
    {
        doc.WriteTo(xw);
        xw.Flush();
        xml = xmlSettings.Encoding.GetString(ms.ToArray());
    }
    return xml;
}

The cause of the bug is as follows, in case you decide to report an issue. As you have noticed, Json.NET represents the XML text value for an element without attributes differently from the text value for an element with attributes:

  • No Attributes: {"timestamp":"2016-11-15T01:07:14"}.

    In this case, the JSON token value for your date string is added to the XML DOM via the method XmlNodeConverter.CreateElement():

        if (reader.TokenType == JsonToken.String
            || reader.TokenType == JsonToken.Integer
            || reader.TokenType == JsonToken.Float
            || reader.TokenType == JsonToken.Boolean
            || reader.TokenType == JsonToken.Date)
        {
            string text = ConvertTokenToXmlValue(reader);
            if (text != null)
            {
                element.AppendChild(document.CreateTextNode(text));
            }
        }
    

    It calls ConvertTokenToXmlValue():

    private string ConvertTokenToXmlValue(JsonReader reader)
    {
        if (reader.TokenType == JsonToken.String)
        {
            return (reader.Value != null) ? reader.Value.ToString() : null;
        }
        else if (reader.TokenType == JsonToken.Integer)
        {
    #if !(NET20 || NET35 || PORTABLE || PORTABLE40)
            if (reader.Value is BigInteger)
            {
                return ((BigInteger)reader.Value).ToString(CultureInfo.InvariantCulture);
            }
    #endif
    
            return XmlConvert.ToString(Convert.ToInt64(reader.Value, CultureInfo.InvariantCulture));
        }
        else if (reader.TokenType == JsonToken.Float)
        {
            if (reader.Value is decimal)
            {
                return XmlConvert.ToString((decimal)reader.Value);
            }
            if (reader.Value is float)
            {
                return XmlConvert.ToString((float)reader.Value);
            }
    
            return XmlConvert.ToString(Convert.ToDouble(reader.Value, CultureInfo.InvariantCulture));
        }
        else if (reader.TokenType == JsonToken.Boolean)
        {
            return XmlConvert.ToString(Convert.ToBoolean(reader.Value, CultureInfo.InvariantCulture));
        }
        else if (reader.TokenType == JsonToken.Date)
        {
    #if !NET20
            if (reader.Value is DateTimeOffset)
            {
                return XmlConvert.ToString((DateTimeOffset)reader.Value);
            }
    #endif
    
            DateTime d = Convert.ToDateTime(reader.Value, CultureInfo.InvariantCulture);
    #if !PORTABLE
            return XmlConvert.ToString(d, DateTimeUtils.ToSerializationMode(d.Kind));
    #else
            return XmlConvert.ToString(d);
    #endif
        }
        else if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }
        else
        {
            throw JsonSerializationException.Create(reader, "Cannot get an XML string value from token type '{0}'.".FormatWith(CultureInfo.InvariantCulture, reader.TokenType));
        }
    }
    

    Which has a lot of logic for converting a JSON token value to an XML value and is doing the right thing when converting your dates and times to XML.

  • Attributes:      {"timestamp":{"@id":"1","#text":"2016-11-15T01:07:14"}}.

    But in this case the current JSON token value gets appended as-is to the XML DOM in the method DeserializeValue():

    private void DeserializeValue(JsonReader reader, IXmlDocument document, XmlNamespaceManager manager, string propertyName, IXmlNode currentNode)
    {
        switch (propertyName)
        {
            case TextName:
                currentNode.AppendChild(document.CreateTextNode(reader.Value.ToString()));
                break;
    

    As you can see, the conversion logic is missing and ToString() is used instead. That's the bug.

    Replacing that line with the following fixes your problem:

                currentNode.AppendChild(document.CreateTextNode(ConvertTokenToXmlValue(reader)));