JSON and Nullable elements: Difference between revisions

From DataFlex Wiki
Jump to navigationJump to search
No edit summary
No edit summary
 
(6 intermediate revisions by the same user not shown)
Line 1: Line 1:
=== The nullable JSON issue ===
=== The nullable JSON issue ===
Using DataFlex it is very convenient to fill up your JSON data from a struct when using the [https://docs.dataaccess.com/dataflexhelp/mergedProjects/VDFClassRef/cJsonObject.htm cJSONOjbect] class. See for example our [[Create JSON from struct]] article.
Using DataFlex it is very convenient to fill up your JSON data from a struct when using the [https://docs.dataaccess.com/dataflexhelp/mergedProjects/VDFClassRef/cJsonObject.htm 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.
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 [https://docs.dataaccess.com/dataflexhelp/mergedProjects/VDFClassRef/cJsonObject-Procedure-RemoveMember.htm RemoveMember] method.
There's also the issue of receiving JSON data with a null in there and not being able to process that automatically via a struct as DataFlex variables are not nullable.
 
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 process by using the [https://docs.dataaccess.com/dataflexhelp/mergedProjects/VDFClassRef/cJsonObject-Procedure-RemoveMember.htm RemoveMember] method.


<source lang="dataflex">
<source lang="dataflex">
   Send RemoveMember of hoJSON "myElement"
   Send RemoveMember of hoJSON "myElement"
</source>
Please note that when you use JsonToDataType to go from JSON to a struct that you now have to turn off the strict checking if all members are available with
<source lang="dataflex">
  Set pbRequireAllMembers of hoJSON to False
</source>
</source>


Line 79: Line 87:


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..
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..
=== The RemoveNamedMember method ===
So when looking at the line with the actual condition on when to remove a specific element :
<source lang="dataflex">
  If (data.addresses.shipto.latitude=0.0 and data.addresses.shipto.longitude=0.0) Begin
</source>
it looked like it would be good to have a method that could take care of this in one line as well.
eg. this would be nice to have:
<source lang="dataflex">
  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
</source>
it will then also work for a json structure where the address is a level deeper without having to write new code.
eg.
<source lang="dataflex">
    Get RemoveNamedMember hoJsonRequest "Returns.addresses.shipTo.longitude"
</source>
Turned out it was fairly easy to write (easier than writing this post!)
<source lang="dataflex">
 
  //
  // Used to strip a named/value pair from json
  // You can use this 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.
  // Beware that JSON member names are 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
</source>
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 when debugging your code.
Of course a little while later I bump into a more complicated issue where the removed member is in an array. Lucky for me, in an single dimension array. This can be taken care of like this:
<source lang="dataflex">
 
  //
  // Used to strip longitude/latitude data from json in a CreateTransactionModel
  // 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 "newTransaction.addresses[2].shipTo.latitude"
  //
  Procedure RemoveNamedMember Handle hoJson String sName
    Integer iDotPos
    Integer iLSBPos iIndex
    Integer eType
    Handle  hoChild
    Handle  hoArray
    String  sMember sIndex
   
    Move (Pos(".",sName)) to iDotPos
    If (iDotPos>0) Begin
      Move (Left(sName,iDotPos-1)) To sMember
      Move (Replace(sMember+".",sName,"")) To sName
      Move (Pos("[",sMember)) To iLSBPos
      If (iLSBPos>0) Begin
        Move (Right(sMember,(length(sMember)+1)-iLSBPos)) To sIndex
        Move (Replace(sIndex,sMember,"")) To sMember
        Move (Replace("[",sIndex,"")) To sIndex
        Move (Replace("]",sIndex,"")) To sIndex
        Move (Cast(sIndex,Integer))  To iIndex
      End
      //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
      Else If (eType=jsonTypeArray) Begin
        Get Member of hoJson sMember to hoArray
        If (hoArray) Begin
          Get MemberByIndex of hoArray iIndex to hoChild
          If (hoChild) Begin
            Send RemoveNamedMember hoChild sName
            Send Destroy Of hoChild
          End
          Send Destroy Of hoArray
        End
      End
    End
    Else Begin
      Send RemoveMember Of hoJson sName
    End
  End_Procedure
</source>


=== Harm's RemoveEmptyMembers method ===
=== Harm's RemoveEmptyMembers method ===
Line 144: Line 280:


That works, but I'm personally a bit cautious about removing all elements that have value "0".  
That works, but I'm personally a bit cautious about removing all elements that have value "0".  
Sometimes we want pass that actual value, empty or zero does not equal null. There might be another element with the value 0 that should not be removed.
Sometimes we want pass that actual value, empty or zero does not equal null. There might be another element with the value 0 that should not be removed. However, sometimes it is exactly what you need.


Personally I would rather remove a specified element.
If you just want to remove the nulls before you move the data to a struct, then perhaps Mike Peat's variant of Harm's code is a bit more clear:


=== The RemoveNamedMember method ===
So when looking at the line with the actual condition on when to remove a specific element :
<source lang="dataflex">
<source lang="dataflex">
  If (data.addresses.shipto.latitude=0.0 and data.addresses.shipto.longitude=0.0) Begin
// Version of Harm's procedure, just for nulls.
</source>
Procedure RemoveNullMembers Handle hoJsonObj
it looked like it would be good to have a method that could take care of this in one line as well.
    Integer iTo iMember iType
    Handle hoMember
    String sMemberName
    Boolean bRemove
   
    Get MemberCount of hoJsonObj to iTo
    Decrement iTo


eg. this would be nice to have:
    For iMember from 0 to iTo
<source lang="dataflex">
        Move False to bRemove
  If (data.addresses.shipto.latitude=0.0 and data.addresses.shipto.longitude=0.0) Begin
        Get MemberByIndex of hoJsonObj iMember to hoMember
    Get RemoveNamedMember hoJsonRequest "addresses.shipTo.latitude"
        Get JsonType of hoMember to iType
    Get RemoveNamedMember hoJsonRequest "addresses.shipTo.longitude"
       
  End
        If (iType = jsonTypeNull) ;
</source>
                Move True to bRemove
        Else If (iType = jsonTypeObject or iType = jsonTypeArray) ;
                Send RemoveNullMembers hoMember
       
        If bRemove Begin


it will then also work for a json structure where the address is a level deeper without having to write new code.
            If (IsOfJsonType(hoJsonObj, jsonTypeObject)) Begin
                Get MemberNameByIndex of hoJsonObj iMember to sMemberName
                Send RemoveMember of hoJsonObj sMemberName
                Decrement iMember
                Decrement iTo
            End
            Else If (IsOfJsonType(hoJsonObj, jsonTypeArray)) Begin
                Send RemoveMember of hoJsonObj iMember
                Decrement iMember
                Decrement iTo
            End


eg.
        End
<source lang="dataflex">
       
     Get RemoveNamedMember hoJsonRequest "Returns.addresses.shipTo.longitude"
        Send Destroy of hoMember
</source>
     Loop


Turned out it was fairly easy to write (easier than writing this post!)
End_Procedure
<source lang="dataflex">
 
  //
  // Used to strip a named/value pair from json
  // You can use this 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.
  // Beware that JSON member names are 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
</source>
</source>
 
See also (3)
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 when debugging your code.


=== External references ===
=== External references ===
Line 219: Line 332:
* (1) [https://support.dataaccess.com/Forums/showthread.php?64157-YAFR-DataTypeToJson-beyond-19-1 YAFR DataTypeToJson beyond 19.1] loong and mostly off topic debate at the forum about null support and JSON
* (1) [https://support.dataaccess.com/Forums/showthread.php?64157-YAFR-DataTypeToJson-beyond-19-1 YAFR DataTypeToJson beyond 19.1] loong and mostly off topic debate at the forum about null support and JSON
* (2) [https://support.dataaccess.com/Forums/showthread.php?62437-JsonToDataType&p=331876#post331876 JsonToDataType] Harm Wibier's solution
* (2) [https://support.dataaccess.com/Forums/showthread.php?62437-JsonToDataType&p=331876#post331876 JsonToDataType] Harm Wibier's solution
* (3) [https://support.dataaccess.com/Forums/showthread.php?64923-Null-json&p=348369#post348369 Json-Nulls] Mike Peat his variant
* [https://support.dataaccess.com/Forums/showthread.php?62358-Use-of-HasMember-of-cJSON-class-when-used-with-arrays&p=331630#post331630 RemoveNulls function by Mike Peat]


[[Category:REST]]
[[Category:JSON]]
[[Category:JSON]]
[[Category:System Integration]]

Latest revision as of 22:46, 8 March 2024

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.

There's also the issue of receiving JSON data with a null in there and not being able to process that automatically via a struct as DataFlex variables are not nullable.

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 process by using the RemoveMember method.

  Send RemoveMember of hoJSON "myElement"

Please note that when you use JsonToDataType to go from JSON to a struct that you now have to turn off the strict checking if all members are available with

  Set pbRequireAllMembers of hoJSON to False

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
   },
   "shipFrom": {
     ... more details ...
   }
 }}

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..

The RemoveNamedMember method

So when looking at the line with the actual condition on when to remove a specific element :

  If (data.addresses.shipto.latitude=0.0 and data.addresses.shipto.longitude=0.0) Begin

it looked like it would be good 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 without having to write new code.

eg.

    Get RemoveNamedMember hoJsonRequest "Returns.addresses.shipTo.longitude"

Turned out it was fairly easy to write (easier than writing this post!)

  
  //
  // Used to strip a named/value pair from json
  // You can use this 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.
  // Beware that JSON member names are 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 when debugging your code.

Of course a little while later I bump into a more complicated issue where the removed member is in an array. Lucky for me, in an single dimension array. This can be taken care of like this:

  
  //
  // Used to strip longitude/latitude data from json in a CreateTransactionModel
  // 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 "newTransaction.addresses[2].shipTo.latitude"
  //
  Procedure RemoveNamedMember Handle hoJson String sName
     Integer iDotPos
     Integer iLSBPos iIndex
     Integer eType
     Handle  hoChild
     Handle  hoArray
     String  sMember sIndex
     
     Move (Pos(".",sName)) to iDotPos
     If (iDotPos>0) Begin
       Move (Left(sName,iDotPos-1)) To sMember
       Move (Replace(sMember+".",sName,"")) To sName
       Move (Pos("[",sMember)) To iLSBPos
       If (iLSBPos>0) Begin
         Move (Right(sMember,(length(sMember)+1)-iLSBPos)) To sIndex
         Move (Replace(sIndex,sMember,"")) To sMember
         Move (Replace("[",sIndex,"")) To sIndex
         Move (Replace("]",sIndex,"")) To sIndex
         Move (Cast(sIndex,Integer))   To iIndex
       End
       //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
       Else If (eType=jsonTypeArray) Begin
         Get Member of hoJson sMember to hoArray
         If (hoArray) Begin
           Get MemberByIndex of hoArray iIndex to hoChild
           If (hoChild) Begin
             Send RemoveNamedMember hoChild sName
             Send Destroy Of hoChild
           End
           Send Destroy Of hoArray
         End
       End
     End
     Else Begin
       Send RemoveMember Of hoJson sName
     End
  End_Procedure

Harm's RemoveEmptyMembers method

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. See also (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, empty or zero does not equal null. There might be another element with the value 0 that should not be removed. However, sometimes it is exactly what you need.

If you just want to remove the nulls before you move the data to a struct, then perhaps Mike Peat's variant of Harm's code is a bit more clear:

// Version of Harm's procedure, just for nulls.
Procedure RemoveNullMembers Handle hoJsonObj
    Integer iTo iMember iType
    Handle hoMember
    String sMemberName
    Boolean bRemove
    
    Get MemberCount of hoJsonObj to iTo
    Decrement iTo

    For iMember from 0 to iTo
        Move False to bRemove
        Get MemberByIndex of hoJsonObj iMember to hoMember
        Get JsonType of hoMember to iType
         
        If (iType = jsonTypeNull) ;
                Move True to bRemove
        Else If (iType = jsonTypeObject or iType = jsonTypeArray) ;
                Send RemoveNullMembers hoMember
         
        If bRemove Begin

            If (IsOfJsonType(hoJsonObj, jsonTypeObject)) Begin
                Get MemberNameByIndex of hoJsonObj iMember to sMemberName
                Send RemoveMember of hoJsonObj sMemberName
                Decrement iMember
                Decrement iTo
            End
            Else If (IsOfJsonType(hoJsonObj, jsonTypeArray)) Begin
                Send RemoveMember of hoJsonObj iMember
                Decrement iMember
                Decrement iTo
            End

        End
         
        Send Destroy of hoMember
    Loop

End_Procedure

See also (3)

External references