Search code examples
c#xmlserializationxmlserializerubl

how to assign "id" property a type other than string


I implement objects for their further serialization in xml code.

sample code i should get:

    <cbc:ID schemeID="urn:oioubl:id:taxschemeid-1.1">63</cbc:ID>

this is how i implemented it in c#:

    public class TaxScheme
    {
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CDC)]
        public TaxSchemeId ID { get; set; }

        //...
    }

    public class TaxSchemeId
    {
        [XmlAttribute("schemeID")]
        public string SchemeID { get; set; }

        [XmlText]
        public string Value { get; set; }

        public TaxSchemeId(string value)
        {
            SchemeID = "urn:oioubl:id:taxschemeid-1.1";
            Value = value;
        }

        public TaxSchemeId()
        {

        }
    }

I get this error on startup:

InvalidOperationException: The top XML element 'ID' from namespace 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2' references distinct types System.String and ex1.V1.Tax.TaxSchemeId. Use XML attributes to specify another XML name or namespace for the element or types.

if you comment out the "ID" property, then there are no problems with serialization

i tried to do it with attributes:


        [XmlElement(ElementName ="ID",Namespace = XML_Namespaces_OIOUBL2.CDC)]
        public TaxSchemeId TaxSchemeId { get; set; }

&

        [XmlElement(ElementName ="ID", Type = typeof(TaxSchemeId),Namespace =    XML_Namespaces_OIOUBL2.CDC)]
        public TaxSchemeId TaxSchemeId { get; set; }

but didn't help.

the id property should only write in large, because I need to implement the OIOUBL standard.

from ideas that is but did not try: change property name (eg TaxSchemeId) -> take the resulting xml file -> and replace the name with the desired one (.Replace("TaxSchemeId", "ID"))

UPD code to run the problem is that I use the name ID more than once:

using System;
using System.Xml.Serialization;
using System.Xml;
using System.IO;


// sample code i want to get
    //< cac:TaxTotal >
    //  < cbc:TaxAmount currencyID = "DKK" > 37.50 </ cbc:TaxAmount >
    //  < cac:TaxSubtotal >
    //    < cbc:TaxableAmount currencyID = "DKK" > 150.00 </ cbc:TaxableAmount >
    //    < cbc:TaxAmount currencyID = "DKK" > 37.50 </ cbc:TaxAmount >
    //    < cac:TaxCategory >
    //      < cbc:ID schemeID = "urn:oioubl:id:taxcategoryid-1.1" schemeAgencyID="320">StandardRated</cbc:ID >
    //      < cbc:Percent > 25.00 </ cbc:Percent >
    //      < cac:TaxScheme >
    //        < cbc:ID schemeID = "urn:oioubl:id:taxschemeid-1.1" > 63 </ cbc:ID >
    //        < cbc:Name > Moms </ cbc:Name >
    //        < cbc:CurrencyCode > DKK </ cbc:CurrencyCode >
    //      </ cac:TaxScheme >
    //    </ cac:TaxCategory >
    //  </ cac:TaxSubtotal >
    //</ cac:TaxTotal >

public class Program
{
    public static void Main()
    {
        Invoice invoice = new Invoice()
        {
            TaxTotal = new TaxTotal()
            {
                TaxAmount = new MonetaryValue("DKK", 40.00M),
                TaxSubtotal = new TaxSubtotal()
                {
                    TaxableAmount = new MonetaryValue("DKK", 40.00M),
                    TaxAmount = new MonetaryValue("DKK", 40.00M),
                    TaxCategory = new TaxCategory()
                    {
                        ID = new TaxCategoryId(),
                        Percent = 25.00m,
                        TaxScheme = new TaxScheme()
                        {
                            ID = new TaxSchemeId("63"),
                            Name = "Moms",
                            CurrencyCode = "DKK"
                        }
                    }
                }
            }
        };

        SerializeAndPrintToConsole(invoice);
        Console.WriteLine("Hello World");
    }

    public static void SerializeAndPrintToConsole(Invoice taxScheme)
    {
        XmlSerializer serializer = new XmlSerializer(typeof(Invoice));

        XmlSerializerNamespaces namespaces = new XmlSerializerNamespaces();
        namespaces.Add("cac", "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2");
        namespaces.Add("cbc", "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2");
        namespaces.Add("ccts", "urn:oasis:names:specification:ubl:schema:xsd:CoreComponentParameters-2");
        namespaces.Add("udt", "urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2");
        namespaces.Add("", "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2");

        StringWriter stringWriter = new StringWriter();

        serializer.Serialize(stringWriter, taxScheme, namespaces);

        string xmlString = stringWriter.ToString();

        Console.WriteLine("Serialized XML:");
        Console.WriteLine(xmlString);
    }
}


public class TaxScheme
{
    [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CDC)]
    public TaxSchemeId ID { get; set; }

    [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CDC)]
    public string Name { get; set; }

    [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CDC)]
    public string CurrencyCode { get; set; }
}

public class TaxSchemeId
{
    [XmlAttribute("schemeID")]
    public string SchemeID { get; set; }

    [XmlText]
    public string Value { get; set; }

    public TaxSchemeId(string value)
    {
        SchemeID = "urn:oioubl:id:taxschemeid-1.1";
        Value = value;
    }

    public TaxSchemeId() { }
}

[XmlRoot(Namespace = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2")]
public class Invoice
{
    [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CAC)]
    public TaxTotal TaxTotal { get; set; }
}

public class TaxTotal
{
    [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CDC)]
    public MonetaryValue TaxAmount { get; set; }

    [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CAC)]
    public TaxSubtotal TaxSubtotal { get; set; }
}

public class TaxSubtotal
{
    [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CDC)]
    public MonetaryValue TaxableAmount { get; set; }

    [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CDC)]
    public MonetaryValue TaxAmount { get; set; }

    [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CAC)]
    public TaxCategory TaxCategory { get; set; }
}

public class TaxCategory
{
    [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CDC)]
    public TaxCategoryId ID { get; set; }

    [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CDC)]
    public decimal Percent { get; set; }

    [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CAC)]
    public TaxScheme TaxScheme { get; set; }
}

public class TaxCategoryId
{
    [XmlAttribute("schemeID")]
    public string SchemeID { get; set; }

    [XmlAttribute("schemeAgencyID")]
    public string SchemeAgencyID { get; set; }

    [XmlText]
    public string Value { get; set; }

    public TaxCategoryId()
    {
        SchemeID = "urn:oioubl:id:taxcategoryid-1.1";
        SchemeAgencyID = "320";
        Value = "test";
    }
}

public class MonetaryValue
{
    [XmlAttribute("currencyID")]
    public string CurrencyID { get; set; }

    [XmlText]
    public decimal Value { get; set; }

    public MonetaryValue(string currencyID, decimal value)
    {
        CurrencyID = currencyID;
        Value = value;
    }

    public MonetaryValue(){}
}

public static class XML_Namespaces_OIOUBL2
{
    public const string CDC = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2";
    public const string CAC = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2";
}

Solution

  • You seem to have encountered a bug in XmlSerializer. If I simply add [XmlRoot(Namespace = XML_Namespaces_OIOUBL2.CDC)] to either TaxScheme or TaxCategory:

    [XmlRoot(Namespace = XML_Namespaces_OIOUBL2.CDC)] // Mysterious fix
    public class TaxScheme
    {
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CDC)]
        public TaxSchemeId ID { get; set; }
    
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CDC)]
        public string Name { get; set; }
    
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CDC)]
        public string CurrencyCode { get; set; }
    }
    

    Then the problem goes away. See demo fiddle #1 here.

    You may wish to report an issue about this, as the problem seems related but not identical to:

    Unfortunately this is not a correct fix, because, according to http://www.datypic.com/, cac:TaxScheme and cac:TaxCategory should both be in your XML_Namespaces_OIOUBL2.CAC namespace (i.e. urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2) when serialized independently, rather than XML_Namespaces_OIOUBL2.CDC. And, unfortunately, applying [XmlRoot(Namespace = XML_Namespaces_OIOUBL2.CAC)] to either or both types does not resolve the problem.

    As an alternative, I noticed that your two types TaxCategoryId and TaxSchemeId correspond to the same UBL 2.1 type cbc:ID. So I introduced an ID type corresponding to cbc:ID along with some factory methods to create them:

    public static class IDFactory
    {
        public static ID CreateForTaxCategory(string value) => 
            new ID { SchemeID = "urn:oioubl:id:taxcategoryid-1.1", SchemeAgencyID = "320", Value = value };
        public static ID CreateForTaxScheme(string value) =>
            new ID { SchemeID = "urn:oioubl:id:taxschemeid-1.1", Value = value };
    }
    
    //http://www.datypic.com/sc/ubl21/e-cbc_ID.html
    [XmlRoot("ID", Namespace = XML_Namespaces_OIOUBL2.CBC)]
    public class ID : IdentifierType { }
    
    [XmlRoot("IdentifierType", Namespace = XML_Namespaces_OIOUBL2.CCT)]
    public class IdentifierType
    {
        [XmlAttribute("schemeID")]
        public string SchemeID { get; set; }
    
        [XmlAttribute("schemeName")]
        public string SchemeName { get; set; }
    
        [XmlAttribute("schemeAgencyID")]
        public string SchemeAgencyID { get; set; }
        
        [XmlAttribute("schemeAgencyName")]
        public string SchemeAgencyName { get; set; }
        
        [XmlAttribute("schemeVersionID")]
        public string SchemeVersionID { get; set; }
        
        [XmlAttribute("schemeDataURI")]
        public string SchemeDataURI { get; set; }
    
        [XmlAttribute("schemeURI")]
        public string SchemeURI { get; set; }
        
        [XmlText]
        public string Value { get; set; }
    }
    
    public static class XML_Namespaces_OIOUBL2
    { 
        public const string CBC = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"; // Renamed from CDC
        public const string CAC = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2";
        public const string CCT = "urn:un:unece:uncefact:data:specification:CoreComponentTypeSchemaModule:2";
    }
    

    And modified your data model to use this new type and to include the appropriate XmlRootAttribute.Namespace values:

    //http://www.datypic.com/sc/ubl21/e-ns39_Invoice.html
    [XmlRoot(Namespace = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2")]
    public class Invoice
    {
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CAC)]
        public TaxTotal TaxTotal { get; set; }
    }
    
    //http://www.datypic.com/sc/ubl21/e-cac_TaxTotal.html
    [XmlRoot(Namespace = XML_Namespaces_OIOUBL2.CAC)]
    public class TaxTotal
    {
        //http://www.datypic.com/sc/ubl21/e-cbc_TaxAmount.html
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CBC)]
        public MonetaryValue TaxAmount { get; set; }
    
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CAC)]
        public TaxSubtotal TaxSubtotal { get; set; }
    }
    
    //http://www.datypic.com/sc/ubl21/e-cac_TaxSubtotal.html
    [XmlRoot(Namespace = XML_Namespaces_OIOUBL2.CAC)]
    public class TaxSubtotal
    {
        //http://www.datypic.com/sc/ubl21/e-cbc_TaxableAmount.html
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CBC)]
        public MonetaryValue TaxableAmount { get; set; }
    
        //http://www.datypic.com/sc/ubl21/e-cbc_TaxAmount.html
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CBC)]
        public MonetaryValue TaxAmount { get; set; }
    
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CAC)]
        public TaxCategory TaxCategory { get; set; }
    }
    
    //http://www.datypic.com/sc/ubl21/e-cac_TaxCategory.html
    [XmlRoot(Namespace = XML_Namespaces_OIOUBL2.CAC)]
    public class TaxCategory
    {
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CBC)]
        public ID ID { get; set; }
    
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CBC)]
        public decimal Percent { get; set; }
    
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CAC)]
        public TaxScheme TaxScheme { get; set; }
    }
    
    //http://www.datypic.com/sc/ubl21/e-cac_TaxScheme.html
    [XmlRoot(Namespace = XML_Namespaces_OIOUBL2.CAC)]
    public class TaxScheme
    {
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CBC)]
        public ID ID { get; set; }
    
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CBC)]
        public string Name { get; set; }
    
        [XmlElement(Namespace = XML_Namespaces_OIOUBL2.CBC)]
        public string CurrencyCode { get; set; }
    }
    
    // Note this seems to corresond to both 
    // http://www.datypic.com/sc/ubl21/e-cbc_TaxAmount.html
    // And http://www.datypic.com/sc/ubl21/e-cbc_TaxableAmount.html
    [XmlRoot(Namespace = XML_Namespaces_OIOUBL2.CBC)]
    public class MonetaryValue
    {
        [XmlAttribute("currencyID")]
        public string CurrencyID { get; set; }
    
        [XmlText]
        public decimal Value { get; set; }
    
        public MonetaryValue(string currencyID, decimal value)
        {
            CurrencyID = currencyID;
            Value = value;
        }
    
        public MonetaryValue(){}
    }
    

    Then created an Invoice as follows:

    Invoice invoice = new Invoice()
    {
        TaxTotal = new TaxTotal()
        {
            TaxAmount = new MonetaryValue("DKK", 37.50M),
            TaxSubtotal = new TaxSubtotal()
            {
                TaxableAmount = new MonetaryValue("DKK", 150.00M),
                TaxAmount = new MonetaryValue("DKK", 37.50M),
                TaxCategory = new TaxCategory()
                {
                    ID = IDFactory.CreateForTaxCategory("StandardRated"),
                    Percent = 25.00m,
                    TaxScheme = new TaxScheme()
                    {
                        ID = IDFactory.CreateForTaxScheme("63"),
                        Name = "Moms",
                        CurrencyCode = "DKK"
                    }
                }
            }
        }
    };
    

    And now your SerializeAndPrintToConsole(Invoice taxScheme) was able to serialize the modified data model using XmlSerializer successfully. This appears to be a correct fix, as all C# types are assigned to correct UBL 2.1 namespaces.

    Notes:

    • Your type MonetaryValue is being used for both cbc:TaxAmount and cbc:TaxableAmount. While their contents are the same, their type names are not, so you may need to separate them in the future.

    • I renamed XML_Namespaces_OIOUBL2.CDC to XML_Namespaces_OIOUBL2.CBC to be consistent with UBL 2.1 naming conventions.

    Demo fiddle #2 here.