JSON and Nullable elements
The nullable JSON issue
Using DataFlex it is very convenient to fill up your JSON data from a struct when using the cJSONOjbect class. See for example our Create JSON from struct article.
Quite often that JSON data is then send to a REST service and depending on that service they might not want to receive all the elements in your JSON in order to work correctly.
This has been debated before in the forum (1) a few times and currently the official way to do so is by traversing the JSON data structure and then removing the members that the other side does not want to receive using the RemoveMember method.
Send RemoveMember of hoJSON "myElement"
That works fine for simple JSON structures. If your structure has another level then your code becomes more difficult to write pretty fast.
Take for example the following simplified JSON
{ "type": "Sales", "companyCode": "DEMO", "date": "2013-06-29", "customerCode": "ABC", "addresses": { "shipTo": { "line1": "143 Main Street", "city": "Irvine", "region": "CA", "country": "US", "postalCode": "92614", "longitude": 0.0, "latitude": 0.0 } }}
If that's filled in from a struct and your REST service gets confused if you pass both address as well as long/lat coordinates with value 0.0 then you have to remove the longitude/latitude elements.
To do this your code could look like:
.. Get Create (RefClass(cJsonObject)) to hoJsonRequest If (hoJsonRequest) Begin Send DataTypeToJson Of hoJsonRequest data If (data.addresses.shipto.latitude=0.0 and data.addresses.shipto.longitude=0.0) Begin Send RemoveAddressCoordinates hoJsonRequest "shipTo" "latitude" Send RemoveAddressCoordinates hoJsonRequest "shipTo" "longitude" End ..
with the RemoveAddressCoordinates method looking like this:
// // Used to strip longitude/latitude data from json // Procedure RemoveAddressCoordinates Handle hoJsonRequest String sAddressType String sCoord Integer eType Handle hoAddresses hoAddressType Get MemberJsonType of hoJsonRequest "addresses" to eType If (eType=jsonTypeObject) Begin Get Member of hoJsonRequest "addresses" to hoAddresses If (hoAddresses) Begin Get MemberJsonType of hoAddresses sAddressType to eType // "shipTo" If (eType=jsonTypeObject) Begin Get Member Of hoAddresses sAddressType To hoAddressType If (hoAddressType) Begin Send RemoveMember Of hoAddressType sCoord // "latitude" Send Destroy Of hoAddressType End End Send Destroy Of hoAddresses End End End_Procedure
Yes it's readable, No it's not so easy to support once you get multiple levels deep for your address element as you now have to make sure you destroy the objects etc..
Harm's alternative
Harm Wibier posted a solution where you can automatically remove elements which have a value of 0 or in the case of a string element where the string element is an empty string. (2)
// // Recursive function going over JSON objects removing all empty strings, 0 and NULL values of objects (not from arrays). // // Params: // hoJsonObj Handle of the JSON object to process. // bEmptyString If true empty string values will be removed. // bZeroNumber If true all numeric 0 values will be removed. // bNull If true all NULL values will be removed. // Procedure RemoveEmptyMembers Handle hoJsonObj Boolean bEmptyString Boolean bZeroNumber Boolean bNull Integer iTo iMember iType Handle hoMember String sMemberName Boolean bRemove Get MemberCount of hoJsonObj to iTo For iMember from 0 to (iTo - 1) Move False to bRemove Get MemberByIndex of hoJsonObj iMember to hoMember Get JsonType of hoMember to iType Case Begin Case (iType = jsonTypeString) If (bEmptyString) Begin Move (JsonValue(hoMember) = "") to bRemove End Case Break Case (iType = jsonTypeInteger) If (bZeroNumber) Begin Move (JsonValue(hoMember) = 0) to bRemove End Case Break Case (iType = jsonTypeNull) Move bNull to bRemove Case Break Case (iType = jsonTypeObject or iType = jsonTypeArray) Begin Send RemoveEmptyMembers hoMember bEmptyString bZeroNumber bNull Case Break End Case End // Only remove empty members of objects (not from arrays) If (bRemove and IsOfJsonType(hoJsonObj, jsonTypeObject)) Begin Get MemberNameByIndex of hoJsonObj iMember to sMemberName Send RemoveMember of hoJsonObj sMemberName Decrement iMember Decrement iTo End Send Destroy of hoMember Loop End_Procedure // Use like this: Send RemoveEmptyMembers hoJson False False True
That works, but I'm personally a bit cautious about removing all elements that have value "0". Sometimes we want pass that actual value. I'd rather remove the element I specify.
RemoveNamedMember
So when looking at this line:
If (data.addresses.shipto.latitude=0.0 and data.addresses.shipto.longitude=0.0) Begin
I figured that I wanted to have a method that could take care of this in one line as well.
eg. this would be nice to have:
If (data.addresses.shipto.latitude=0.0 and data.addresses.shipto.longitude=0.0) Begin Get RemoveNamedMember hoJsonRequest "addresses.shipTo.latitude" Get RemoveNamedMember hoJsonRequest "addresses.shipTo.longitude" End
it will then also work for a json structure where the address is a level deeper:
Get RemoveNamedMember hoJsonRequest "Returns.addresses.shipTo.longitude"
Turned out it was fairly easy to write (easier than writing this post!)
// // Used to strip data from json // You can use it to directly remove a JSON member at a lower level from the JSON // object passed via hoJSON. // The member to be removed uses the exact JSON member names separated by dots and // this is case sensitive. // // If the member does not exist a runtime error will be triggered. // Eg. // Send RemoveNamedMember hoJsonRequest "Order.addresses.shipTo.latitude" // Procedure RemoveNamedMember Handle hoJson String sName Integer iDotPos Integer eType Handle hoChild String sMember Move (Pos(".",sName)) to iDotPos If (iDotPos>0) Begin Move (Left(sName,iDotPos-1)) To sMember Move (Replace(sMember+".",sName,"")) To sName //Get HasMember of hoJson sMember to bHasMember <-- use this if you want to filter the runtime error (we currently do not) Get MemberJsonType of hoJson sMember to eType If (eType=jsonTypeObject) Begin Get Member of hoJson sMember to hoChild If (hoChild) Begin Send RemoveNamedMember hoChild sName Send Destroy Of hoChild End End End Else Begin Send RemoveMember Of hoJson sName End End_Procedure
Note that if you don't want the runtime error that you can use the HasMember test. We don't use that as we always first convert from struct so we know the element exists. If the element is not there then there's likely a case sensitivity issue, so getting a runtime error helps debugging.
External references
- (1) YAFR DataTypeToJson beyond 19.1 loong and mostly off topic debate at the forum about null support and JSON
- (2) JsonToDataType Harm Wibier's solution