Roman Borovets
Roman Borovets

Reputation: 858

C# JsonConvert SerializeXmlNode empty with attributes

I'm using JsonConvert SerializeXmlNode to convert xml to json. The problem that I'm facing with is that I have a tag which sometimes can have value and sometimes be null

<AustrittDatum>2018-01-31+01:00</AustrittDatum>
...
<AustrittDatum xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>

And as result - I'm getting an exception when trying to deserialize json to C# object with string property "AustrittDatum" - "Newtonsoft.Json.JsonReaderException: 'Error reading string. Unexpected token: StartObject. Path 'AustrittDatum'.' ", because

<AustrittDatum xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance xsi:nil="true"/> 

is serialized to

"AustrittDatum": {
  "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
  "@xsi:nil": "true"
},

How can I force it to be something like this "AustrittDatum": "" or maybe ther is some proper way to resolve it?

Upvotes: 6

Views: 3947

Answers (1)

dbc
dbc

Reputation: 116981

It seems as though, when encountering an XML element with xsi:nil="true", Json.NET's XmlNodeConverter creates a JSON object with the attributes you see rather than a null JToken. This is consistent with the Newtonsoft documentation page Converting between JSON and XML:

Converstion Rules

  • Elements remain unchanged.
  • Attributes are prefixed with an @ and should be at the start of the object.
  • Single child text nodes are a value directly against an element, otherwise they are accessed via #text.
  • The XML declaration and processing instructions are prefixed with ?.
  • Character data, comments, whitespace and significant whitespace nodes are accessed via #cdata-section, #comment, #whitespace and #significant-whitespace respectively.
  • Multiple nodes with the same name at the same level are grouped together into an array.
  • Empty elements are null.

If the XML created from JSON doesn't match what you want, then you will need to convert it manually...

Nevertheless it's reasonable to think that an element with xsi:nil="true" would be converted to a null JSON value, since xsi:nil is a predefined w3c attribute. Possibly Newtonsoft did not do this because such elements can carry additional attributes, which would be lost if the element were to be converted to null.

You could file an enhancement request for XmlNodeConverter if you like, but in the meantime the following extension methods will post-process a JToken hierarchy and convert objects that were formerly nil elements to null JSON values:

public static class JTokenExtensions
{
    const string XsiNamespace = @"http://www.w3.org/2001/XMLSchema-instance";
    readonly static string XmlNullValue = System.Xml.XmlConvert.ToString(true);

    public static JToken ReplaceXmlNilObjectsWithNull(this JToken root)
    {
        return root.ReplaceXmlNilObjects(t => JValue.CreateNull());
    }

    public static JToken ReplaceXmlNilObjects(this JToken root, Func<JToken, JToken> getReplacement)
    {
        var query = from obj in root.DescendantsAndSelf().OfType<JObject>()
                    where obj.Properties().Any(p => p.IsNilXmlTrueProperty())
                    select obj;
        foreach (var obj in query.ToList())
        {
            var replacement = getReplacement(obj);
            if (obj == root)
                root = replacement;
            if (obj.Parent != null)
                obj.Replace(replacement);
        }
        return root;
    }

    static IEnumerable<JToken> DescendantsAndSelf(this JToken node)
    {
        // Small wrapper adding this method to all JToken types.
        if (node == null)
            return Enumerable.Empty<JToken>();
        var container = node as JContainer;
        if (container != null)
            return container.DescendantsAndSelf();
        else
            return new[] { node };
    }

    static string GetXmlNamespace(this JProperty prop)
    {
        if (!prop.Name.StartsWith("@"))
            return null;
        var index = prop.Name.IndexOf(":");
        if (index < 0 || prop.Name.IndexOf(":", index+1) >= 0)
            return null;
        var ns = prop.Name.Substring(1, index - 1);
        if (string.IsNullOrEmpty(ns))
            return null;
        var nsPropertyName = "@xmlns:" + ns;
        foreach (var obj in prop.AncestorsAndSelf().OfType<JObject>())
        {
            var nsProperty = obj[nsPropertyName];
            if (nsProperty != null && nsProperty.Type == JTokenType.String)
                return (string)nsProperty;
        }
        return null;
    }

    static bool IsNilXmlTrueProperty(this JProperty prop)
    {
        if (prop == null)
            return false;
        if (!(prop.Value.Type == JTokenType.String && (string)prop.Value == "true"))
            return false;
        if (!(prop.Name.StartsWith("@") && prop.Name.EndsWith(":nil")))
            return false;
        var ns = prop.GetXmlNamespace();
        return ns == XsiNamespace;
    }
}

Then use it like:

// Parse XML to XDocument
var xDoc = XDocument.Parse(xmlString);

// Convert the XDocument to an intermediate JToken hierarchy.
var converter = new Newtonsoft.Json.Converters.XmlNodeConverter { OmitRootObject = true };
var rootToken = JObject.FromObject(xDoc, JsonSerializer.CreateDefault(new JsonSerializerSettings { Converters = { converter } } ))
    // And replace xsi:nil objects will null JSON values
    .ReplaceXmlNilObjectsWithNull();

// Deserialize to the final RootObject.
var rootObject = rootToken.ToObject<RootObject>();

Which generates:

"AustrittDatum": [
  "2018-01-31+01:00",
  null
],

Here I am initially parsing to an XDocument but you could also use the older XmlDocument

Sample working .Net fiddle.

Upvotes: 5

Related Questions