Monday, August 12, 2024

Copying and Auto populating financial dimension from inventSite X++

 APPROACH 1

//calling method

DimensionAttributeValueSetStorage  valueStorage = this.getDefaultDimension(inventsite);

purchTable.defaultdimension =  valueStorage.save();


//Method to create DimensionAttributeValueSetStorage 

public DimensionAttributeValueSetStorage getDefaultDimension(InventSite  inventSite)

    {

        DimensionDefault                            result;

        int                                         i;

        DimensionAttribute                          dimensionAttribute, dimensionAttribute1, dimensionAttribute2, dimensionAttribute3;

        DimensionAttribute                          dimensionAttribute4, dimensionAttribute5, dimensionAttribute6;

        DimensionAttributeValue                     dimensionAttributeValue, dimensionAttributeValue1, dimensionAttributeValue2;

        DimensionAttributeValue                     dimensionAttributeValue3, dimensionAttributeValue4, dimensionAttributeValue5;

        DefaultDimensionView                        defaultDimensionView;

        container                                   dimAttr, dimValue;

        str                                         dimValues;

        DimensionAttributeValueSetStorage           valueSetStorage = new DimensionAttributeValueSetStorage();

        DimensionAttributeValueDerivedDimensions    derivedDim, derivedDim2;

        InventParameters                            inventParameters;

            

        select firstonly SiteDimensionAttribute from inventParameters

            join RecId,Name from dimensionAttribute6

                where dimensionAttribute6.RecId == inventParameters.SiteDimensionAttribute;


        select firstonly DefaultDimension, Name, DisplayValue from defaultDimensionView

            where defaultDimensionView.DefaultDimension == inventSite.DefaultDimension

            &&    defaultDimensionView.Name == dimensionAttribute6.Name;


        dimAttr     += [defaultDimensionView.Name];

        dimValue    += [defaultDimensionView.DisplayValue];


        select firstonly DimensionKeyColumnName, RecId from dimensionAttribute

                where dimensionAttribute.DimensionKeyColumnName == defaultDimensionView.Name

                    join firstonly DimensionAttribute, DimensionAttributeValue, DerivedDimensionAttributeValue1, DerivedDimensionAttributeValue2 from derivedDim

                        where derivedDim.DimensionAttribute == dimensionAttribute.RecId

                            join DisplayValue, RecId from dimensionAttributeValue

                                where dimensionAttributeValue.DisplayValue == defaultDimensionView.DisplayValue

                                && derivedDim.DimensionAttributeValue == dimensionAttributeValue.RecId;


        select firstonly DisplayValue, DimensionAttribute from dimensionAttributeValue1

                where derivedDim.DerivedDimensionAttributeValue1 == dimensionAttributeValue1.RecId

                    join firstonly RecId, Name from dimensionAttribute1

                        where dimensionAttributeValue1.DimensionAttribute == dimensionAttribute1.RecId;


        dimAttr     += [dimensionAttribute1.Name];

        dimValue    += [dimensionAttributeValue1.DisplayValue];


        select firstonly DimensionKeyColumnName from dimensionAttribute4

                where dimensionAttribute4.DimensionKeyColumnName == dimensionAttribute1.Name

                join firstonly DimensionAttribute, DimensionAttributeValue, DerivedDimensionAttributeValue1 from derivedDim2

                    where derivedDim2.DimensionAttribute == dimensionAttribute1.RecId

                    join firstonly DisplayValue, Recid from dimensionAttributeValue4

                        where dimensionAttributeValue4.DisplayValue == dimensionAttributeValue1.DisplayValue

                        && derivedDim2.DimensionAttributeValue == dimensionAttributeValue4.RecId;


        select firstonly DimensionAttribute, RecId, DisplayValue from dimensionAttributeValue5

                where derivedDim2.DerivedDimensionAttributeValue1 == dimensionAttributeValue5.RecId

                    join firstonly RecId, Name from dimensionAttribute5

                    where dimensionAttributeValue5.DimensionAttribute == dimensionAttribute5.RecId;


        dimAttr     += [dimensionAttribute5.Name];

        dimValue    += [dimensionAttributeValue5.DisplayValue];


        select firstonly DimensionAttribute, RecId, DisplayValue from dimensionAttributeValue2

                where derivedDim.DerivedDimensionAttributeValue2 == dimensionAttributeValue2.RecId

                    join firstonly Name, RecId from dimensionAttribute2

                    where dimensionAttributeValue2.DimensionAttribute == dimensionAttribute2.RecId;


        dimAttr     += [dimensionAttribute2.Name];

        dimValue    += [dimensionAttributeValue2.DisplayValue];


        for (i = 1; i <= conLen(dimAttr); i++)

        {

            dimensionAttribute3 = DimensionAttribute::findByName(conPeek(dimAttr,i));

            if (dimensionAttribute3.RecId == 0)

            {

                continue;

            }

            dimValues = conPeek(dimValue,i);

            if (dimValues != "")

            {

                dimensionAttributeValue3 = DimensionAttributeValue::findByDimensionAttributeAndValue

                        (dimensionAttribute3,dimValues,false,true);

                valueSetStorage.addItem(dimensionAttributeValue3);

            }

        }

        return valueSetStorage;

    }


    APPROACH 2

If Dimension link is activated on site 

purchTable.DefaultDimension = InventSite::changeDimension(purchTable.DefaultDimension, InventLocation::find(_avaTmpPurchLine.InventLocationId).InventSiteId); 

Thursday, July 25, 2024

Different types of query range value assignment (Expressions and conditions as range value) X++ D365FO

 In the example below, we construct a query and add a single datasource.


The range is then added, using the DataAreaId field on each table. Any field can be used, but using an unusual one such as DataAreaId helps remind a casual reader of the code that it’s not a normal range.


query = new Query();

dsInventTable = query.addDataSource(tableNum(InventTable));

 

// Add our range

queryBuildRange = dsInventTable.addRange(fieldNum(InventTable, DataAreaId));

Given the above, the following are valid range specifications:


Simple criteria[edit]

Find the record where ItemId is B-R14. Take note of the single quotes and parenthesis surrounding the entire expression.


queryBuildRange.value(strFmt('(ItemId == "%1")', queryValue("B-R14")));

Find records where the ItemType is Service. Note the use of any2int().


queryBuildRange.value(strFmt('(ItemType == %1)', any2int(ItemType::Service)));

Find records where the ItemType is Service or the ItemId is B-R14. Note the nesting of the parenthesis in this example.


queryBuildRange.value(strFmt('((ItemType == %1) || (ItemId == "%2"))', 

    any2int(ItemType::Service),

    queryValue("B-R14")));

Find records where the modified date is after 1st January 2000. Note the use of Date2StrXpp() to format the date correctly.


queryBuildRange.value(strFmt('(ModifiedDate > %1)', Date2StrXpp(01\01\2000)));

Find records where the Field is blank (null) or an empty string. For more see Sys::Query Docs[1]


qbrStatement = this.query().dataSourceName("BankAccountTrans2").addRange(fieldnum(BankAccountTrans,AccountStatement));

//qbrStatement.value("!?*");//this is the old way that may not work in future versions of AX

qbrStatement.value(sysquery::valueEmptyString());//this is the new way

Complex criteria with combined AND and OR clauses[edit]

Find all records where the ItemType is Service, or both the ItemType is Item and the ProjCategoryId is Spares. This is not possible to achieve using the standard range syntax.


Note also that in this example, we are using the fieldStr() method to specify our actual field names and again, that we have nested our parenthesis for each sub-expression.


queryBuildRange.value(strFmt('((%1 == %2) || ((%1 == %3) && (%4 == "%5")))',

    fieldStr(InventTable, ItemType),

    any2int(ItemType::Service),

    any2int(ItemType::Item),

    fieldStr(InventTable, ProjCategoryId),

    queryValue("Spares")));

WHERE clauses referencing fields from multiple tables[edit]

For this example below, we construct a query consisting of two joined datasources (using an Exists join). Note that we specify the datasource names when adding the datasources to the query.


The ranges are then added, using the DataAreaId field on each table as described in the earlier example.


query = new Query();

dsInventTable = query.addDataSource(tableNum(InventTable), tableStr(InventTable));

dsInventItemBarCode = dsInventTable.addDataSource(tableNum(InventItemBarCode), tableStr(InventItemBarCode));

dsInventItemBarCode.relations(true);

dsInventItemBarCode.joinMode(JoinMode::ExistsJoin);

 

// Add our two ranges

queryBuildRange1 = dsInventTable.addRange(fieldNum(InventTable, DataAreaId));

queryBuildRange2 = dsInventItemBarCode.addRange(fieldNum(InventItemBarCode, DataAreaId));

Find all records where a bar code record exists for an item and was modified later than the item was modified.


In this example, we are using the range on the BarCode table. Therefore the unqualified ModifiedDate reference will relate to InventItemBarCode.ModifiedDate. The other field is a fully-qualified one, using the DatasourceName.FieldName syntax.


queryBuildRange2.value(strFmt('(ModifiedDate > InventTable.ModifiedDate)'));

Note that if we had added our InventTable datasource using the following code


dsInventTable = query.addDataSource(tableNum(InventTable), "InventTableCustomName"); // Note that we are manually specifying a different datasource name

then the query range would need to appear as follows


queryBuildRange2.value(strFmt('(ModifiedDate > InventTableCustomName.ModifiedDate)'));

Conditional joins[edit]

We will modify our previous example slightly, to remove the automatic addition of relations for the join.


query = new Query();

dsInventTable = query.addDataSource(tableNum(InventTable), "InventTable");

dsInventItemBarCode = dsInventTable.addDataSource(tableNum(InventItemBarCode), "InventItemBarCode");

dsInventItemBarCode.joinMode(JoinMode::ExistsJoin);

 

// Add our two ranges

queryBuildRange1 = dsInventTable.addRange(fieldNum(InventTable, DataAreaId));

queryBuildRange2 = dsInventItemBarCode.addRange(fieldNum(InventItemBarCode, DataAreaId));

We can now use the query expression to specify whatever we like as the join criteria.


Find all records where either the ItemType is Service, or the ItemType is Item and a barcode exists. The join criteria is only applied in the second half of the expression, so all Service items will appear irrespective of whether they have a bar code. Priot to Ax 2012, this was not possible to achieve using the standard query ranges. From that version onwards, however, the QueryFilter class can be used to achieve the same result.


queryBuildRange2.value(strFmt('((%1.%2 == %3) || ((%1.%2 == %4) && (%1.%5 == %6)))',

    query.dataSourceTable(tableNum(InventTable)).name(), // InventTable %1

    fieldStr(InventTable, ItemType), // ItemType %2

    any2int(ItemType::Service), // %3

    any2int(ItemType::Item), // %4

    fieldStr(InventTable, ItemId), // ItemId %5

    fieldStr(InventItemBarCode, ItemId))); // %6

Using the techniques above, it is possible to create queries with almost as much flexibility as using SQL statements directly.


Filter on array fields[edit]

queryBuildRange.value(strFmt('((%1.%2 == "%4") || (%1.%3 == "%5"))', 

    queryBuildDataSource.name(),

    fieldid2name(tablenum(<table>), fieldid2ext(fieldnum(<table>, Dimension), Dimensions::code2ArrayIdx(SysDimension::Center))), 

    fieldid2name(tablenum(<table>), fieldid2ext(fieldnum(<table>, Dimension), Dimensions::code2ArrayIdx(SysDimension::Purpose))), 

    "some dim2 value", 

    "some dim3 value"));

Note: you must always specify the datasource name if you use Query Expression syntax to filter on array fields. See also Limitations section at the bottom of the page.


Using wildcards and comma-separated range values[edit]

Again, the previous example here was using standard syntax, not the special syntax using expressions. It’s not possible to modify the above examples to work with wildcards.


The above statement applies to AX versions < 5.0


AX 5.0 introduced solution to wildcards – while you still cannot directly use wildcards in ranges, now it supports the ‘LIKE’ keyword.


(AccountNum LIKE "*AA*" || Name LIKE "*AA*")

Limitations[edit]

The use of the extended query syntax is not supported by the new having filtering available in Ax 2012.


There are two major limitations to the Query Expressions syntax. The first is the loss of support for wildcards and comma-separated range values, and the second is the inability to reference array fields such as dimensions in some older kernel versions.


Whilst in standard queries you can specify “AA*” or “A,B,C” as criteria, and they will be parsed by Axapta and sent through correctly to the database, these will simply be passed directly through when using the Query Expressions engine. As a result, they will not return the expected results. On a related noted, the use of ‘like’ is not supported, so there is no way to use wildcards in any form.


Query Expressions syntax for array fields such as the Dimension field is known to be suppported since the version 5.0.1500.2116 (RU4) for AX 2009. Previous kernel versions are not tested to support Query Expressions syntax for array fields; it is also known not to work at all in Axapta 3.


There is a discussion regarding the use of array fields on the Discussion page for this article. Please contribute to that discussion if possible!

Monday, July 1, 2024

JSON and XML messages deserialization in D365 Finance and Operations

 In this article, I will explain concepts of JSON and XML message deserialization into X++ class contracts for D365 Finance and Operations (FO). And of course, the implementation of it.

A short introduction to the topic

As these days cloud ERP system like D365 FO, provides much more functionality than “old” Axapta, in a complex world of distributed services. It is more dependent on communication with external systems than ever. However, the tooling that is provided and the legacy of X++, puts a lot of grief in complex Integration with the outside world.

Since AX7 has been put into the Azure cloud, the exchange of text files (of various structures) became a nuisance. As we move towards a growing number of API integrations, it puts more pressure on using open standards like JSON and XML.

As I would like to abstract from how the message with XML/JSON information comes into the system, there are many ways of doing this. I will cover only topics dealing with the text content of such a message.

Deserialization of JSON messages into X++ contract classes

The easiest way will be showing this on Sales Order example. Which is most common and familiar concept for everybody interested in this topic.

Let assume the simplest information to be integrated to FO. Which will be Sales Order with basic information and few Sales Lines.

No alt text provided for this image


Sales Order header containing information:

  • Sales Number - tag "SalesNumber" - simple String
  • Customer Name – tag: “Name” - simple type String
  • Customer Account ID - tag: “Account” – simple type String
  • Delivery Date – tag: “DeliveryDate” – simple type String(using of string and parsing it depended of agreed date model is a safe policy that prevents lot of troubles)
  • Delivery Address – tag “DeliveryAddress” -complex type for Address
  • Sales Order lines – tag “Lines” – List of complex type for Sales Line

Implementation of SOHeader contract:

X++
//Sales Order Header Contract
[DataContractAttribute]
class SOHeader
{
    str salesNumber;
    str custName;
    str custAccount;
    str deliveryDate;
    SOAddress deliveryAddress;
    List SOLines;


    [DataMemberAttribute('SalesNumber')]
    public str parmSalesNumber(str _salesNumber = salesNumber)
    {
        salesNumber = _salesNumber;
        return salesNumber;
    }

    [DataMemberAttribute('Name')]
    public str parmCustName(str _CustName = custName)
    {
        custName = _CustName;
        return custName;
    }


    [DataMemberAttribute('Account')]
    public str parmCustAccount(str _CustAccount = custAccount)
    {
        custAccount = _CustAccount;
        return custAccount;
    }


    [DataMemberAttribute('DeliveryDate')]
    public str parmDeliveryDate(str _deliveryDate = deliveryDate)
    {
        deliveryDate = _deliveryDate;
        return deliveryDate;
    }


    [DataMemberAttribute('DeliveryAddress')]
    public SOAddress parmDeliveryAddress(SOAddress _deliveryAddress = deliveryAddress)
    {
        deliveryAddress = _deliveryAddress;
        return deliveryAddress;
    }


    [DataMemberAttribute('Lines'), DataCollectionAttribute(Types::Class, classStr(SOLine))]
    public List parmSOLines(List _SOLines = SOLines)
    {
        SOLines = _SOLines;
        return SOLines;
    }


}

Important parts of this are [Attributes]

[DataContractAttribute] - class atribute indicating that it is a Contract class

[DataMemberAttribute('Name')] or [DataMemberAttribute('DeliveryAddress')] - object attribute - looks the same for simple and complex objects. Return object from such method has to be defined accordingly to type simple or class

 [DataMemberAttribute('Lines'), DataCollectionAttribute(Types::Class, classStr(SOLine))] - Collection attribute which indicate that after that there will be list of objects for defined class. Return object for such method has to be defined as "List"

Value in Attribute e.g. "Name" indicates exact JSON tag, not Class method name or class local variable name. So in JSON it would look like {"Name":"Bulb Inc. ELSON"}.

When processing FormJsonSerializer will find this tag("Name"), decode method name "parmCustName" -as this one has it as DataMemberAttribute - and will assign it to class variable "custName"

Implementation of SOAddress contract:

  • Address Name - tag "AddressName"
  • Street - tag "Street"
  • City - tag "City"
  • ZipCode - tag "PostCode"
  • CountryCode - tag "ISOCountry"

X++
//Sales Order Header Delivery Address Contract
[DataContractAttribute]
class SOAddress
{
    str AName;
    str AStreet;
    str ACity;
    str AZipCode;
    str ACountryCode; 


    [DataMemberAttribute('AddressName')]
    public str parmName(str _AddressName = AName)
    {
        AName = _AddressName;
        return AName;
    }


    [DataMemberAttribute('Street')]
    public str parmStreet(str _AStreet = AStreet)
    {
        AStreet = _AStreet;
        return AStreet;
    }


    [DataMemberAttribute('City')]
    public str parmCity(str _ACity = ACity)
    {
        ACity = _ACity;
        return ACity;
    }


    [DataMemberAttribute('PostCode')]
    public str parmZipCode(str _AZipCode = AZipCode)
    {
        AZipCode = _AZipCode;
        return AZipCode;
    }


    [DataMemberAttribute('ISOCountry')]
    public str parmCountryCode(str _ACountryCode = ACountryCode)
    {
        ACountryCode = _ACountryCode;
        return ACountryCode;
    }


} 

And last Contract is SOLine:

  • Item Name - tag "ItemName"
  • ItemCode - tag "ItemCode"
  • LineNum - tag "LineID" - type int
  • Price - tag "Price" type real - item price
  • Qty - tag "Qty" - type real - sales Qty
  • UnitID - tag "UnitID" - unit for sales Qty
  • CurrencyCode - tag "CurrencyISO" - Currency Code for the price

X++
//Sales Order Line Contract
[DataContractAttribute]
class SOLine
{
    str ItemName;
    str ItemCode;
    int LineNum;
    real Price;
    real Qty;
    str UnitID;
    str CurrencyCode;



    [DataMemberAttribute('ItemName')]
    public str parmItemName(str _AddressName = ItemName)
    {
        ItemName = _AddressName;
        return ItemName;
    }


    [DataMemberAttribute('ItemCode')]
    public str parmItemCode(str _ItemCode = ItemCode)
    {
        ItemCode = _ItemCode;
        return ItemCode;
    }


    [DataMemberAttribute('LineID')]
    public int parmLineNum(int _lineNum = LineNum)
    {
        LineNum = _lineNum;
        return LineNum;
    }


    [DataMemberAttribute('Price')]
    public real parmPrice(real _Price = Price)
    {
        Price = _Price;
        return Price;
    }


    [DataMemberAttribute('Qty')]
    public real parmQty(real _Qty = Qty)
    {
        Qty = _Qty;
        return Qty;
    }


    [DataMemberAttribute('UnitID')]
    public str parmUnitID(str _UnitID = UnitID)
    {
        UnitID = _UnitID;
        return UnitID;
    }


    [DataMemberAttribute('CurrencyISO')]
    public str parmCurrencyCode(str _CurrencyCode = CurrencyCode)
    {
        CurrencyCode = _CurrencyCode;
        return CurrencyCode;
    }


}

Those 3 simple contracts allows us to deserialize input JSON message like that:

JSON
{
  "SalesNumber" : "S0001"
  "Account":"UK00BL1",
  "Name":"Bulb Inc. ELSON",
  "DeliveryDate":"19/07/2021",
  "DeliveryAddress":
  {
    "AddressName":"Blub Inc UK",
    "City":"ELSON",
    "ISOCountry":"GB",
    "PostCode":"",
    "Street":"85 Whitchurch Road"
  },
  "Lines":[
    {
       "LineID":1,
       "ItemName":"Bulb 100W",     
       "ItemCode":"BL00100W",   
       "Price":1.5,
       "CurrencyISO":"GBP",
       "Qty":3,
       "UnitID":"ea"
    },
    {
       "LineID":2,
       "ItemName":"Bulb LED 10W",     
       "ItemCode":"LED0010W",   
       "Price":15.45,
       "CurrencyISO":"GBP",
       "Qty":2,       
       "UnitID":"box"
    }
    ]
}

Using just one line of code..

X++
SOHeader SOHeader = FormJsonSerializer::deserializeObject(classNum(SOHeader), serializedJSONstr);

If we want to serialize SOHeader into JSON with values it is one line as well..

X++
str serializedJSON = FormJsonSerializer::serializeClass(SOHeader);

Easy peasy lemon squeezy.. or is it? Check the conclusions at the end :)

Transformation of XML into JSON

Since Microsoft provided us with such a great tool for JSON. One could imagine it will be the same with XML.. but it is not.

However JSON is younger brother of XML ,which mean that transformation from one to another, has been sorted many moons ago. So to deal with XML, we just need to transform it to JSON or back from JSON.

It can be done in 2 ways.. "simple" way as external library in .NET Framework referenced in X++ or D365FO "helper" class in D365 FO.

Simple way of deserializing XML to JSON in .NET Framework using Newtownsof.Json requires just 3 lines of code:

C# Code
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml);


string json = JsonConvert.SerializeXmlNode(doc, Newtonsoft.Json.Formatting.Indented, true);

However it cannot be done like that in X++.. as we can't use SerializeXMLNode we have to use SerializeXnode which is not that pleasant:

X++
stringreader = new System.IO.StringReader(tmpString);
xmlReader = new System.Xml.XmlTextReader(stringreader);
xmlReader.Read();

newNode = System.Xml.Linq.XNode::ReadFrom(xmlReader);

retJSON =  Newtonsoft.Json.JsonConvert::SerializeXNode(newNode, Newtonsoft.Json.Formatting::None); 

But to make it really work, we need to do a lot of cleansing manually, as output is a bit rubbish. SerializeXNode also does not allow XML encoding header, so the code need to have a lot of input/output fixes

X++
class SOXMLHelper
{
 
  
    public static str deserializeXML2JSON(str _xmlString)
    {
        System.Xml.XmlTextReader    xmlReader;
        System.IO.StringReader      stringReader;
        System.Xml.Linq.XNode newNode;
        Newtonsoft.Json.Formatting	formatting;
        str							tmpString;
        str							retJSON;
  
        try
        {
            tmpString = strReplace(_xmlString, "\r\n", "");
            tmpString = SOXMLHelper::removeXMLHeader(tmpString);// additional method to deal with XML encoding header <?xml ?>


            stringreader = new System.IO.StringReader(tmpString);
            xmlReader = new System.Xml.XmlTextReader(stringreader);
            xmlReader.Read();


            newNode = System.Xml.Linq.XNode::ReadFrom(xmlReader);


            retJSON =  Newtonsoft.Json.JsonConvert::SerializeXNode(newNode, Newtonsoft.Json.Formatting::None);


            retJSON = SOXMLHelper::cleanCarriageReturn(retJSON); //XNode is creating lots of carriege returns that will be transformed to JSON objects which we don't want


            retJSON = SOXMLHelper::cleanEmptyJSONArrays(retJSON);//XNnode is creating those from all empty spaces which are decoded as JSON collections we don't want either


            xmlReader.Close();
            xmlReader.Dispose();
            stringreader.Dispose();
        }
        catch(Exception::CLRError)
        {
            System.Exception ex = CLRInterop::getLastException();
            throw Error(strFmt("SOXMLHelper::deserializeXML2JSON with error %1", ex.Message));
        }
        catch
        {
            throw Error("SOXMLHelper::deserializeXML2JSON failed");
        }
    
       return retJSON;

    }
}

Luckily serialization from JSON to XML is much easier:

X++
public static str serializeContract2XML(Object _object)
{
        str						serializedJson;
        System.Xml.Linq.XNode	node;
        str						retXML;


        serializedJson = FormJsonSerializer::serializeClass(_object);


        node = Newtonsoft.Json.JsonConvert::DeserializeXNode(serializedJson);
        
        retXML = node.ToString();

        retXML = SOXMLHelper::addXMLHeader(retXML); // we need to add XML header to make it proper XML

        return retXML; 

Dealing with XML attributes in X++ contract classes

Another thing is XML requires something that JSON can live without which is ROOT element.

So when we are serializing or deserializing a defined contract that works fine in JSON.. it will not work for XML, as we need to wrap it in ROOT node. Of course, it applies if our JSON does not have ROOT element. But unfortunately in our case, it does not have one yet.

The new contract has to be added to wrap SORoot so we can deal with XML. In our case, we will call it SORoot with the tag "SalesOrder". Which will point to our existing SOHeader contact.

X++
//Sales Order Header Root Contract
[DataContractAttribute]
class SORoot
{
    SOHeader SOHeader;


    [DataMemberAttribute('SalesOrder')]
    public SOHeader parmSOHeader(SOHeader _SOHeader = SOHeader)
    {
        SOHeader = _SOHeader;
        return SOHeader;
    }


}

"SalesOrder" node in JSON will act as ROOT and wrap SOHeader nicely.

JSON
{
  "SalesOrder": {
  "SalesNumber":"S0001",
  "Account":"UK00BL1",
  "Name":"Bulb Inc. ELSON",
  "DeliveryDate":"19/07/2021",
  "DeliveryAddress":
  {
    "AddressName":"Blub Inc UK",
    "City":"ELSON",
    "ISOCountry":"GB",
    "PostCode":"",
    "Street":"85 Whitchurch Road"
  },
  "Lines":[
    {
       "LineID":1,
       "ItemName":"Bulb 100W",     
       "ItemCode":"BL00100W",   
       "Price":1.5,
       "CurrencyISO":"GBP",
       "Qty":3,
       "UnitID":"ea"
    },
    {
       "LineID":2,
       "ItemName":"Bulb LED 10W",     
       "ItemCode":"LED0010W",   
       "Price":15.45,
       "CurrencyISO":"GBP",
       "Qty":2,       
       "UnitID":"box"
    }
    ]
}} 

So finally we can have proper XML!!

XML
<?xml version="1.0" encoding="utf-8"?>
<SalesOrder>  
  <SalesNumber>S0001</SalesNumber>
  <Account>UK00BL1</Account>
  <Name>Bulb Inc. ELSON</Name>  
  <DeliveryDate>19/07/2021</DeliveryDate>
  <DeliveryAddress>    
    <AddressName>Blub Inc UK</AddressName>    
    <City>ELSON</City>    
    <ISOCountry>GB</ISOCountry>    
    <PostCode>SY12 5DQ</PostCode>    
    <Street>85  Whitchurch Road</Street>  
  </DeliveryAddress>    
  <Lines>
    <LineID>1</LineID>   
    <ItemCode>BL00100W</ItemCode>    
    <ItemName>Bulb 100W</ItemName>          
    <Price>1.5</Price> 
    <CurrencyISO>GBP</CurrencyISO>        
    <Qty>3</Qty>    
    <UnitID>ea</UnitID>  
  </Lines>  
  <Lines> 
   <LineID>2</LineID>
   <ItemCode>LED0010</ItemCode>    
   <ItemName>Bulb LED 10W</ItemName>           
   <Price>15.45</Price>    
   <CurrencyISO>GBP</CurrencyISO>
   <Qty>2</Qty>    
   <UnitID>box</UnitID>  
  </Lines>  
</SalesOrder> 

We could end up with XML here..

But what if some legacy system would like to play with XML attributes? As so far we have operated only on simple XML nodes with values only.

E.g. for Sales Line contract instead "tag->value" pair we will have some values and "XML Attributes"

XML
<Lines>   
  <ItemCode>LED0010</ItemCode>    
  <ItemName>Bulb LED 10W</ItemName>    
  <LineID>2</LineID>    
  <Price>15.45</Price>  
  <CurrencyISO>GBP</CurrencyISO>    
  <Qty>2</Qty>    
  <UnitID>box</UnitID>  
</Lines>

becomes:

<Lines>   
  <ItemCode>LED0010</ItemCode>    
  <ItemName>Bulb LED 10W</ItemName>    
  <LineID>2</LineID>    
  <Price CurrencyISO="GBP">15.45</Price>    
  <Qty UnitID="box">2</Qty>
</Lines> 

The answer is self explanatory as soon as we will check, what will happen when we will transform changed XML into JSON.

JSON
{
  "ItemCode":"LED0010",
  "ItemName":"Bulb LED 10W",
  "LineID":2,
  "Price":15.45,
  "CurrencyISO":"GBP",
  "Qty":2,
  "UnitID":"box"
}

becomes:
{
  "ItemCode": "LED0010",
  "ItemName": "Bulb LED 10W",
  "LineID": "2",
  "Price": {
    "@CurrencyISO": "GBP",
     "#text": "15.45"
  },
  "Qty": {
    "@UnitID": "box",
    "#text": "2"
  }
} 

So our simple Types in Sales Line like "Qty" -> becomes contract objects. And as we remember from previous paragraph about Contract attributes, that value in attribute is JSON tag

X++
//Sales Order Line Contract
[DataContractAttribute]
class SOLine
{
    str ItemName;
    str ItemCode;
    int LineNum;
    real Price;
    real Qty;
    str UnitID;    
    str CurrencyCode;
}

has to be modified to
[DataContractAttribute]
class SOLine
{
    str ItemName;
    str ItemCode;
    int LineNum;
    SOPrice Price;
    SOQty Qty;
}
and we need need 2 new Contract classes

[DataContractAttribute]
class SOPrice 
{
   str TextValue;
   str CurrencyISOAttr;


    [DataMemberAttribute("@CurrencyISO")]
    public str parmCurrencyISOAttr(str _Code = CurrencyISOAttr)
    {
        CurrencyISOAttr = _Code;
        return CurrencyISOAttr;
    }


    [DataMemberAttribute("#text")]
    public str parmText(str _Text = TextValue
    {
        TextValue= _Text;
        return TextValue;
    }  
  
  }
and we will need another one for SOQty which will have exat the same format value is #text and attribute has @ in front of attibute tag

IMHO using attributes in XML for D365 FO purposes make sense only if we really have to...

E.g. We have to deal with legacy system that deals only with XML. And workaround with changes of legacy system, is to much effort, so we are biting the bullet in X++.

As when we are playing with attributes JSON becomes much more complex needs more processing to be decoded.

Fixes to FormJsonSerializer

Our base in all those exercises is playing with JSON string. And to do that we are doomed to use FormJsonSerializer. However when we use it, we need to remember that it works only with "strong defined" contracts. By which I mean that all fields, objects and collections has to be defined in X++ contract classes for deserialization. Don't worry about serialization as this is always straight forward.

So if external system will start to send new extra tags, or we have missed some tags in initial definition. Our input JSON deserialization into X++ contract classes will fail. As FormJsonSerializer deserialization will fail processing on those not defined. And will finish reading prematurely so our output will be incomplete.

If we want to make it work, we need to fix method deserializeObjectInternal(). And as we can't change it standard one for obvious reasons(no way of extending it). We need our own Deserializer. So we have to create new CUSTFormJsonSerializer copy functionality from FormJsonSerializer and fix those bits:

X++ // Part of  FormJsonSerializer.deserializeObjectInternal()
..
else if (jsonReader.TokenType == Newtonsoft.Json.JsonToken::StartArray)
            {
                // This is the case for collection type properties
                if(currentJsonProperty)
                {
                    if(dataMembers.exists(currentJsonProperty))
                    {
                        // Determine if the property has a colleciton attribute
                        objectMethod = dataMembers.lookup(currentJsonProperty);
                        collectionAttribute = objectMethod.getAttribute(classStr(DataCollectionAttribute));
                        if(collectionAttribute)
                        {
                            // Deserialize the collection
                            propertyValue = CUSTFormJsonSerializer ::deserializeCollectionInternal(
                                objectMethod.returnId(),
                                collectionAttribute.itemType(),
                                collectionAttribute.itemTypeName(),
                                jsonReader);
                            // Set the property
                            objectType.callObject(objectMethod.name(), deserializedObject, propertyValue);
                        }
                    }
//WE NEED TO SERVICE EVERY ELEMENT SINCE IT IS A RECURENCE METHOD
					else
                    {
                        CUSTFormJsonSerializer ::deserializeCollectionDummy(jsonReader);
                    }
                }
            }
            else if (jsonReader.TokenType == Newtonsoft.Json.JsonToken::StartObject)
            {
                // This is the case for nested complex type properties
                if(currentJsonProperty)
                {
                    if(dataMembers.exists(currentJsonProperty))
                    {
                        // Read the object from JSON
                        objectMethod = dataMembers.lookup(currentJsonProperty);
                        // Desrialize the object property
                        propertyValue = CUSTFormJsonSerializer ::deserializeObjectInternal(objectMethod.returnId(), jsonReader);
                        // Set the proeprty value
                        objectType.callObject(objectMethod.name(), deserializedObject, propertyValue);
                    }
//WE NEED TO SERVICE EVERY ELEMENT SINCE IT IS A RECURENCE METHOD
					else
                    {
                        CUSTFormJsonSerializer::deserializeObjectDummy(jsonReader);
                    }
                }
            }
..

Conclusions and remarks

Using contract classes is "must have" for complex integrations. Especially if we don't have "middle man" to transform messages for us and 3rd party APIs has to be dealt with as they are.

Transforming XML to JSON and back, can be done on the fly, e.g. when using API Management. But sometimes messages are so big, that we will end up with shuffling this data using AzureBlobs. So we will always need some internal mechanism inside of X++. However IMO proper external libraries(for XML->JSON and back) using newest NuGet packages are better than X++ workarounds. But I have shown both ways in case someone is not comfortable with .dll addons.

And of course there is issue with FormJsonSerializer that has to be addressed...

But when we will overcome all those problems. We can start decoding and creating messages for any external system fast and reliable. More important functional code in D365 FO becomes nice, clean and efficient. Since we don't have to iterate through XML just play with class objects.

I hope that you will find some of those information useful.

Copying and Auto populating financial dimension from inventSite X++

 APPROACH 1 //calling method DimensionAttributeValueSetStorage  valueStorage = this.getDefaultDimension(inventsite); purchTable.defaultdimen...