Search code examples
c#xmlxmlserializer

C# Serializing XmlElement with XmlText


I have this XML format I need to replicate:

<item>
    <attribute1>1</attribute1>
    <attribute2 something="true">
        2
    </attribute2>
    <attribute3 something="false">
        3
    </attribute3>
    <!-- goes on until attribute25 -->
</item>

I'm currently using something like this to achieve what I want:

Item.cs:

[XmlType(TypeName = "item")]
public class Item {

    [XmlElement("attribute1")]
    public CustomElement<string> Attribute1 { get; set; }

    [XmlElement("attribute2")]
    public CustomElement<string> Attribute2 { get; set; }

    [XmlElement("attribute3")]
    public CustomElement<string> Attribute3 { get; set; }

    // Etc Etc
}

CustomElement.cs:

/// <summary>
///     Represents a CustomElement.
/// </summary>
/// <typeparam name="T">The type for the value of the element.</typeparam>

public class CustomElement<T>
{
    [XmlIgnore] public T Value;

    [XmlText]
    public T XmlValue => Value;

    public CustomElement()
    {
    }

    public CustomElement(T value)
    {
        Value = value;
    }

    [XmlIgnore]
    public bool? Something { get; set; }

    [XmlAttribute("something")]
    public bool XmlSomething
    {
        get => Something != null && Something.Value;
        set => Something = value;
    }

    public bool XmlSomethingSpecified => Something.HasValue;

    public static implicit operator CustomElement<T>(T x)
    {
        return new CustomElement<T>(x);
    }
}

However, after serializing my Item I get:

<item>
    <attribute1 />
    <attribute2 />
    <attribute3 />
</item>

How do I fix my CustomElement class so the value doesn't get lost?


Solution

  • You have several problems here:

    1. You are trying to serialize the XmlValue member as the element value for the element generated for an instance of CustomElement<T>, but you have defined it as a read-only expression-bodied member:

      public T XmlValue => Value;
      

      XmlSerializer will only serialize public read/write properties and fields, so you must add a setter as well as a getter:

      [XmlText]
      public T XmlValue { get => Value; set => Value = value; }
      

      Alternatively, to simplify your model you could eliminate XmlValue entirely and serialize Value directly by marking it with [XmlText]. XML serialization attributes can be applied to fields as well as properties.

    2. You are trying to suppress serialization of the <something> element when the underlying value is null using the {propertyName}Specified pattern. This pattern is primarily used for tracking whether or not an associated element is encountered, and so the xxxSpecified property must be marked with [XmlIgnore]:

      [XmlIgnore]
      public bool XmlSomethingSpecified => Something.HasValue;
      

      Or, since you don't actually need to track the presence of the <something> element, you could simplify your model by switching to the ShouldSerialize{PropertyName}() pattern:

      public bool ShouldSerializeXmlSomething() => Something.HasValue;
      

      The differences between the two patterns are described in ShouldSerialize() vs Specified Conditional Serialization Pattern.

    3. If your Item type is going to be the root element of your XML document, you must mark it with [XmlRoot("item")] to make the root element's name be <item>:

      [XmlRoot("item")]
      [XmlType(TypeName = "ci")]
      public class Item
      {
          // Etc Etc
      }
      
    4. In your c# code you have marked the XmlSomething property with [XmlElement] but in your XML something is shown as an element: <something>true</something>.

      If you really want it to be an element you must mark XmlSomething with [XmlElement]:

      [XmlElement("something")]
      public bool XmlSomething
      {
          get => Something != null && Something.Value;
          set => Something = value;
      }
      
    5. In your question, you show an example of a <something> element with a non-boolean textual value:

      <attribute3>
          3
          <something>text</something>
      </attribute3>
      

      I reckon this is a typo in the question, but if not, you will need to redesign your CustomElement<T> type to make Something be a string.

    Sample working Roslyn .Net fiddle, and a second with the suggested simplifications for CustomElement<T>, and a third where <something> appears as a child element.

    The classes from the second fiddle look like:

    public class CustomElement<T>
    {
        [XmlText]
        public T Value;
    
        public CustomElement()
        {
        }
    
        public CustomElement(T value)
        {
            Value = value;
        }
    
        [XmlIgnore]
        public bool? Something { get; set; }
    
        [XmlAttribute("something")]
        public bool XmlSomething
        {
            get => Something != null && Something.Value;
            set => Something = value;
        }
    
        public bool ShouldSerializeXmlSomething() => Something.HasValue;
    
        public static implicit operator CustomElement<T>(T x)
        {
            return new CustomElement<T>(x);
        }
    }
    
    [XmlRoot("item")]
    [XmlType(TypeName = "ci")]
    public class Item
    {
        [XmlElement("attribute1")]
        public CustomElement<string> Attribute1 { get; set; }
    
        [XmlElement("attribute2")]
        public CustomElement<string> Attribute2 { get; set; }
    
        [XmlElement("attribute3")]
        public CustomElement<string> Attribute3 { get; set; }
    
        // Etc Etc
    }