Search code examples
c#xmlxml-deserialization

Deserializing result from Web Service call to a child element type


I've connected to a web service and downloading XML which I'm trying to deserialize. However, the XML has a top level element of a different type than the child element that I'm trying to serialize to.

<ServiceResponse xmlns="namespace1">
  <ServiceResult xmlns:i="namespace2">
   ...lots of elements
  </ServiceResult>
</ServiceResponse>

I've written the following code:

public MyData ConvertXmlToMyData(string filepath)
{
     XmlRootAttribute xRoot = new XmlRootAttribute();
     xRoot.ElementName = "ServiceResponse";
     xRoot.Namespace = @"namespace1";
     xRoot.IsNullable = true;

     XmlSerializer reader = new XmlSerializer(typeof(MyData), xRoot);
     StreamReader file = new StreamReader(filepath);

     MyData result = (MyData)reader.Deserialize(file);
     file.Close();
     return result;
}

The code executes with no exception, but all the underlying elements are null. I know this is because ServiceResponse is not the same as ServiceResult, but I don't know how to specify that I want the deserialization to happen on the child element, not the whole object (there are no other elements, just a single child of type ServiceResult).

The only solutions I've seen state that I should edit the declaration of the types, but in my situation I'm getting them from a web service so I can't do that.

Does anyone know what I should be doing instead?

Edit: added details of the MyData annotations from the service reference

[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")]
[System.Runtime.Serialization.DataContractAttribute(Name="MyData", Namespace="namespace2")]
[System.SerializableAttribute()]
public partial class MyData: object, System.Runtime.Serialization.IExtensibleDataObject, System.ComponentModel.INotifyPropertyChanged {

Edit 2:

Changed code to:

public MyData ConvertXmlToMyData(string filepath)
{
    DataContractSerializer reader = new DataContractSerializer(typeof(MyData));

    StreamReader file = new StreamReader(filepath);

    file.BaseStream.Position = 0;

    MyData result = (MyData)reader.ReadObject(file.BaseStream); <- error thrown here

    file.Close();

    return result;
}

Now seeing error:

Error in line 1 position 73. Expecting element 'MyData' from namespace 'namespace1'.. Encountered 'Element' with name 'ServiceResponse', namespace 'namespace1'.


Solution

  • Let's suppose your xml file looks like this:

    <?xml version="1.0" encoding="utf-8" ?>
    <ServiceResponse xmlns="namespace1">
      <ServiceResult xmlns:i="namespace2">
        <desc>1</desc>
      </ServiceResult>
    </ServiceResponse>
    

    I've just added an xml and a desc to your sample.

    Then in order to skip the top-level entity we need to semi-parse the xml. One way to do this:

    1. Load the FileStream into an XDocument
    2. Find the top-level entity
    3. Find the second-level entity
    //Step #1
    var xml = File.OpenRead("sample.xml");
    var semiParsedData = XDocument.Load(xml);
    
    //Step #2
    const string topNamespace = "namespace1", firstNode = "ServiceResponse";
    const StringComparison comparison = StringComparison.InvariantCultureIgnoreCase;
    
    var topLevelEntity = semiParsedData.Descendants().FirstOrDefault(element => 
            string.Equals(element.Name.Namespace.NamespaceName, topNamespace, comparison)
            && string.Equals(element.Name.LocalName, firstNode, comparison));
    
    //Step #3
    const string secondNode = "ServiceResult";
    var secondLevelEntity = topLevelEntity?.Descendants().FirstOrDefault(element =>
        string.Equals(element.Name.Namespace.NamespaceName, topNamespace, comparison)
        && string.Equals(element.Name.LocalName, secondNode, comparison));
    
    if (secondLevelEntity == null)
        return;
    

    Hopefully there is a method called CreateReader, which is defined on the XNode class.

    • The secondLevelEntity is a XElement instance.
    • XElement is inherited from XContainer.
    • XContainer is inherited from XNode.

    So, we can call the CreateReader on the secondLevelEntity:

    using var resultReader = secondLevelEntity.CreateReader();
    

    The resultReader is a XmlReader instance and hopefully the DataContractSerializer's ReadObject has an overload which accepts a XmlReader:

    var deserializer = new DataContractSerializer(typeof(MyData));
    var myData = (MyData)deserializer.ReadObject(resultReader);
    Console.WriteLine(myData.Description);
    

    And finally the data model should look like this:

    [DataContract(Name = "ServiceResult", Namespace = "namespace1")]
    public class MyData
    {
        [DataMember(Name = "desc")]
        public string Description { get; set; }
    }