Microsoft Dynamics 365 for Operations (AX7) has a browser based HTML user interface that's dependent on JavaScript. For browsers to exchange data with the server they must employ a standard for sending text over the network. JSON (JavaScript Object Notation) is one of these standards and it is extensively used in AX7. JSON has the advantage that it is more compact than XML, it takes fewer bytes to transmit the same amount of information. And it is 100% compatible with JavaScript.
Even though JSON is a human readable format, to avoid having to reinvent the wheel, it's generally a good idea to use a standard JSON serializer instead of having to build your own. Luckily for us, AX7 has the FormJSONSerializer class and you can use it to convert information stored in X++ classes as properties into JSON with a minimal amount of effort.
Under the hood, FormJSONSerializer uses the library from Newtonsoft to serialize X++ classes into JSON. JSON.NET is a well-known, sturdy and high-performance library.
With a few lines of code, you can serialize to JSON anywhere in your X++ code, using the newtonsoft library:
System.IO.StringWriter oStringWriter; Newtonsoft.Json.JsonTextWriter oJSONWriter; oStringWriter = new System.IO.StringWriter(); oJSONWriter = new Newtonsoft.Json.JsonTextWriter(oStringWriter); str sJSON = ""; oJSONWriter.WriteStartObject(); oJSONWriter.WritePropertyName("SomeProperty"); oJSONWriter.WriteValue(15); oJSONWriter.WriteEndObject(); sJSON = oStringWriter.ToString();
In the above example, once it is run, the contents of the sJSON variable will be:
{"SomeProperty":15}
But the good thing about the FormJSONSerializer is that it already has code to handle lists and other classes and you can combine data contracts to create complex data structures. All classes that extend FormDataContract can be converted to JSON in AX7 using just a single line of X++ code:
str sJSON = FormJSONSerializer ::serializeClass(oDataContractClass);
At the other end in JavaScript it's incredibly easy to deserialize:
var oData = JSON.parse(sJSONString);
You only have to create the X++ data contract classes. As I've said before a data contract class is an X++ class that extends FormDataContract and has a DataContract attribute.
[DataContractAttribute] class MainClass extends FormDataContract { … }
A data contract class must have one or more properties of the type FormProperty. If you check out the FormProperty class you will find that it has some methods like parmValue and setValue. At its core, FormProperty is just a wrapper for AnyType. A property can be an X++ primitive, a class or a list and has to be declared at the class level as a FormProperty.
FormProperty m_sProperty;
The FormProperty is initialized in the classes' new method:
public void new() { super(); m_sProperty = this.properties().addProperty(methodStr(MainClass, StringProperty), Types::String, "default"); }
Now what we're doing with the code above is to link the FormProperty m_sProperty of type string with the method StringProperty of the MainClass, and setting the default value to "default". This is the method:
[DataMemberAttribute("StringProperty")] public str StringProperty(str _sValue = m_sProperty.parmValue()) { if (!prmIsDefault(_sValue)) { m_sProperty.parmValue(_sValue); } return _sValue; }
There's a thing about this getter/setter method: it can have a different name in X++ and in JSON, the name of the property in JSON being defined by the DataMemberAttribute. Why would you want different names in JSON and X++? Maybe you can't use a reserved name as a method name in X++. Apart from that case I would keep names the same for clarity.
The FormJSONSerializer takes care of automatically serializing these X++ types: str, int, int64, real, utcdatetime, date, enum and guid.
A Sample Data Contract Class
This sample data contract class uses all of the types that FormJSONSerializer supports:
[DataContractAttribute] class AllTypesDataContractClass extends FormDataContract { FormProperty m_sString; FormProperty m_iInteger; FormProperty m_lInteger64; FormProperty m_rReal; FormProperty m_dtUtcDateTime; FormProperty m_dDate; FormProperty m_bBoolean; FormProperty m_yEnum; FormProperty m_uGuid; public void new() { super(); m_sString = this.properties().addProperty(methodStr(AllTypesDataContractClass, StringParm), Types::String, "default"); m_iInteger = this.properties().addProperty(methodStr(AllTypesDataContractClass, IntegerParm), Types::Integer, 543); m_lInteger64 = this.properties().addProperty(methodStr(AllTypesDataContractClass, Integer64Parm), Types::Int64, 6756455345444); m_rReal = this.properties().addProperty(methodStr(AllTypesDataContractClass, RealParm), Types::Real, 6754.67); m_dtUtcDateTime = this.properties().addProperty(methodStr(AllTypesDataContractClass, UtcDateTimeParm), Types::UtcDateTime, DateTimeUtil::newDateTime(3\5\2017, 10800, Timezone::GMTMINUS0800PACIFICTIME)); m_dDate = this.properties().addProperty(methodStr(AllTypesDataContractClass, DateParm), Types::Date, 3\5\2017); m_bBoolean = this.properties().addProperty(methodStr(AllTypesDataContractClass, BooleanParm), Types::Enum, true); m_yEnum = this.properties().addProperty(methodStr(AllTypesDataContractClass, EnumParm), Types::Enum, ABC::A); m_uGuid = this.properties().addProperty(methodStr(AllTypesDataContractClass, GuidParm), Types::Guid, newGuid()); } [DataMemberAttribute("StringParm")] public str StringParm(str _sValue = m_sString.parmValue()) { if(!prmisDefault(_sValue)) { m_sString.parmValue(_sValue); } return _sValue; } [DataMemberAttribute("IntegerParm")] public int IntegerParm(int _iValue = m_iInteger.parmValue()) { if(!prmisDefault(_iValue)) { m_iInteger.parmValue(_iValue); } return _iValue; } [DataMemberAttribute("Integer64Parm")] public int64 Integer64Parm(int64 _lValue = m_lInteger64.parmValue()) { if(!prmisDefault(_lValue)) { m_lInteger64.parmValue(_lValue); } return _lValue; } [DataMemberAttribute("RealParm")] public real RealParm(real _rValue = m_rReal.parmValue()) { if(!prmisDefault(_rValue)) { m_rReal.parmValue(_rValue); } return _rValue; } [DataMemberAttribute("UtcDateTimeParm")] public utcdatetime UtcDateTimeParm(utcdatetime _dtValue = m_dtUtcDateTime.parmValue()) { if(!prmisDefault(_dtValue)) { m_dtUtcDateTime.parmValue(_dtValue); } return _dtValue; } [DataMemberAttribute("DateParm")] public date DateParm(date _dValue = m_dDate.parmValue()) { if(!prmisDefault(_dValue)) { m_dDate.parmValue(_dValue); } return _dValue; } [DataMemberAttribute("BooleanParm")] public boolean BooleanParm(boolean _bValue = m_bBoolean.parmValue()) { if(!prmisDefault(_bValue)) { m_bBoolean.parmValue(_bValue); } return _bValue; } [DataMemberAttribute("EnumParm")] public ABC EnumParm(ABC _yValue = m_yEnum.parmValue()) { if(!prmisDefault(_yValue)) { m_yEnum.parmValue(_yValue); } return _yValue; } [DataMemberAttribute("GuidParm")] public guid GuidParm(guid _uValue = m_uGuid.parmValue()) { if(!prmisDefault(_uValue)) { m_uGuid.parmValue(_uValue); } return _uValue; } }
When the above class is created and serialized with this code:
AllTypesDataContractClass oAllTypesDataContractClass = new AllTypesDataContractClass(); str sJSON = FormJsonSerializer::serializeClass(oAllTypesDataContractClass);
It will set the sJSON variable to this value:
{"BooleanParm":true,"DateParm":"/Date(1493769600000)/","EnumParm":"A","GuidParm":"{D82E3635-5E9A-4CCE-93ED-EEA350FD59B1}","Integer64Parm":6756455345444,"IntegerParm":543,"RealParm":6754.67,"StringParm":"default","UtcDateTimeParm":"/Date(1493805600000)/"}
You will notice that the serializer handles regular enums as well as the Boolean enum.
The Utcdatetime Case.
The type utcdatetime is made up of two elements a date and time and a timezone. In the Dynamics 365 for Finance and Operations SQL server tables it's stored in two columns, a datetime and an int column for the timezone. If we look at the FMPickupAndReturnTable from fleet management, this is the design for the table:
And this is the same table in SQL Server.
Dates depend greatly on the time zone. EDT might be 10:30pm but the same time in PDT is 7:30am. The utcdatetime in FormJSONSerializer is serialized using this code:
private void writeDateTimeValue(utcDateTime _value) { utcDateTime dateTimeValue; int64 jsDateTimeStamp; str jsonDateTime; dateTimeValue = _value; //jsonWriter.WriteValue(dateTimeValue); jsDateTimeStamp = DateTimeUtil::getDifference(dateTimeValue, dateTimeBase) * 1000; jsonDateTime = strFmt("\/Date(%1)\/", jsDateTimeStamp); jsonWriter.WriteValue(jsonDateTime); }
So, it's serializing the date in the GMT time zone and you must take that into account when you deserialize the date in JavaScript.
Serializing Classes and Lists
DataContract classes can contain other classes and lists, this could be the structure representing a person, with a single address and several phone numbers:
PersonDataContract:
[DataContractAttribute] class PersonDataContract extends FormDataContract { FormProperty m_sFirstName; FormProperty m_sLastName; FormProperty m_iAge; FormProperty m_oAddress; FormProperty m_aPhoneNumbers; public void new() { super(); m_sFirstName = this.properties().addProperty(methodStr(PersonDataContract, FirstName), Types::String); m_sLastName = this.properties().addProperty(methodStr(PersonDataContract, LastName), Types::String); m_iAge = this.properties().addProperty(methodStr(PersonDataContract, Age), Types::Integer); m_oAddress = this.properties().addProperty(methodStr(PersonDataContract, Address), Types::Class); m_oAddress.parmValue(new AddressDataContract()); m_aPhoneNumbers = this.properties().addProperty(methodStr(PersonDataContract, PhoneNumbers), Types::Class); m_aPhoneNumbers.parmValue(new List(Types::Class)); } [DataMemberAttribute("FirstName")] public str FirstName(str _sValue = m_sFirstName.parmValue()) { if(!prmisDefault(_sValue)) { m_sFirstName.parmValue(_sValue); } return _sValue; } [DataMemberAttribute("LastName")] public str LastName(str _sValue = m_sLastName.parmValue()) { if(!prmisDefault(_sValue)) { m_sLastName.parmValue(_sValue); } return _sValue; } [DataMemberAttribute("Age")] public int Age(int _iValue = m_iAge.parmValue()) { if(!prmisDefault(_iValue)) { m_iAge.parmValue(_iValue); } return _iValue; } [DataMemberAttribute("Address")] public AddressDataContract Address(AddressDataContract _oValue = m_oAddress.parmValue()) { if(!prmisDefault(_oValue)) { m_oAddress.parmValue(_oValue); } return _oValue; } [DataMemberAttribute('PhoneNumbers'), DataCollectionAttribute(Types::Class, classStr(PhoneDataContract))] public List PhoneNumbers(List _aValue = m_aPhoneNumbers.parmValue()) { if(!prmisDefault(_aValue)) { m_aPhoneNumbers.parmValue(_aValue); } return _aValue; } }
AddressDataContract:
[DataContractAttribute] class AddressDataContract extends FormDataContract { FormProperty m_sStreetAddress; FormProperty m_sCity; FormProperty m_sState; FormProperty m_sPostalCode; public void new() { super(); m_sStreetAddress = this.properties().addProperty(methodStr(AddressDataContract, StreetAddress), Types::String); m_sCity = this.properties().addProperty(methodStr(AddressDataContract, City), Types::String); m_sState = this.properties().addProperty(methodStr(AddressDataContract, State), Types::String); m_sPostalCode = this.properties().addProperty(methodStr(AddressDataContract, PostalCode), Types::String); } [DataMemberAttribute("StreetAddress")] public str StreetAddress(str _sValue = m_sStreetAddress.parmValue()) { if(!prmisDefault(_sValue)) { m_sStreetAddress.parmValue(_sValue); } return _sValue; } [DataMemberAttribute("City")] public str City(str _sValue = m_sCity.parmValue()) { if(!prmisDefault(_sValue)) { m_sCity.parmValue(_sValue); } return _sValue; } [DataMemberAttribute("State")] public str State(str _sValue = m_sState.parmValue()) { if(!prmisDefault(_sValue)) { m_sState.parmValue(_sValue); } return _sValue; } [DataMemberAttribute("PostalCode")] public str PostalCode(str _sValue = m_sPostalCode.parmValue()) { if(!prmisDefault(_sValue)) { m_sPostalCode.parmValue(_sValue); } return _sValue; } }
PhoneDataContract:
[DataContractAttribute] class PhoneDataContract extends FormDataContract { FormProperty m_sType; FormProperty m_sNumber; public void new() { super(); m_sType = this.properties().addProperty(methodStr(PhoneDataContract, Type), Types::String); m_sNumber = this.properties().addProperty(methodStr(PhoneDataContract, Number), Types::String); } [DataMemberAttribute("Type")] public str Type(str _sValue = m_sType.parmValue()) { if(!prmisDefault(_sValue)) { m_sType.parmValue(_sValue); } return _sValue; } [DataMemberAttribute("Number")] public str Number(str _sValue = m_sNumber.parmValue()) { if(!prmisDefault(_sValue)) { m_sNumber.parmValue(_sValue); } return _sValue; } }
With this code you can fill in the PersonDataContract
PersonDataContract oPersonDataContract = new PersonDataContract(); oPersonDataContract.FirstName("John"); oPersonDataContract.LastName("Smith"); oPersonDataContract.Age(25); oPersonDataContract.Address().StreetAddress("21 2nd Street"); oPersonDataContract.Address().City("New York"); oPersonDataContract.Address().State("NY"); oPersonDataContract.Address().PostalCode("10021-3100"); PhoneDataContract oPhoneDataContract; oPhoneDataContract = new PhoneDataContract(); oPhoneDataContract.Type("home"); oPhoneDataContract.Number("212 555-1234"); oPersonDataContract.PhoneNumbers().addEnd(oPhoneDataContract); oPhoneDataContract = new PhoneDataContract(); oPhoneDataContract.Type("office"); oPhoneDataContract.Number("646 555-4567"); oPersonDataContract.PhoneNumbers().addEnd(oPhoneDataContract); oPhoneDataContract = new PhoneDataContract(); oPhoneDataContract.Type("mobile"); oPhoneDataContract.Number("123 456-7890"); oPersonDataContract.PhoneNumbers().addEnd(oPhoneDataContract); str sJSON = FormJsonSerializer::serializeClass(oPersonDataContract);
and serialize it to obtain the following JSON result:
{"Address":{"City":"New York","PostalCode":"10021-3100","State":"NY","StreetAddress":"21 2nd Street"},"Age":25,"FirstName":"John","LastName":"Smith","PhoneNumbers":[{"Number":"212 555-1234","Type":"home"},{"Number":"646 555-4567","Type":"office"},{"Number":"123 456-7890","Type":"mobile"}]}
Using FormJSONSerializer to deserialize JSON objects into X++ will be covered in another article.