Written by

Question Elize VdRiet · Feb 24, 2019

REST fromJSON nested structure date not working, getting "Datatype value is not a number" for a date, not possible to debug in Atelier.

Hi,

I am having a problem in REST service when de-serializing JSON using a nested Property.

I am new to Intersystems and my first time creating REST Service. I setup Atelier dev environment. The worst IDE I have ever worked in. I was able to  attach to a process in the debug perspective and step through the code, but one cannot see contents of variables nor print anything to the console, so it is useless as a debugging tool. When looking at a variable all it displays is its type of the variable and if I try to "watch" I get:

An internal error occurred during: "Label Job".
java.lang.NullPointerException

SO I am unable to solve the issue I am having with the JSON in my REST service due to being unable to debug properly, so I will appreciate any help on this. I try to explain the issue:

I have a Persistent class Consumer which uses 2 SerialObjects as Properties "ActivePeriod" and "AM", I created a REST service to add records to the Consumer, I use Postman to send JSON for a new Consumer.  I get an error back in Postman as follows:

2019-02-01{"Status":"ERROR #7207: Datatype value '2019-02-01' is not a valid number\r\n  > ERROR #5802: Datatype validation failed on property 'Metadata.ActivePeriod:StartDate', with value equal to \"2019-02-01\""}{
    "errors":[ {
            "code":7207,
            "domain":"%ObjectErrors",
            "error":"ERROR #7207: Datatype value '2019-02-01' is not a valid number\r\n  > ERROR #5802: Datatype validation failed on property 'Metadata.ActivePeriod:StartDate', with value equal to \"2019-02-01\"",
            "errors":[ {
                    "code":5802,
                    "domain":"%ObjectErrors",
                    "error":"ERROR #5802: Datatype validation failed on property 'Metadata.ActivePeriod:StartDate', with value equal to \"2019-02-01\"",
                    "id":"DatatypeValidationFailed",
                    "params":["Metadata.ActivePeriod:StartDate","2019-02-01"
                    ]
                }
            ],
            "id":"DTNotNum",
            "params":["2019-02-01"
            ]
        }
    ],
    "summary":"ERROR #7207: Datatype value '2019-02-01' is not a valid number\r\n  > ERROR #5802: Datatype validation failed on property 'Metadata.ActivePeriod:StartDate', with value equal to \"2019-02-01\""
}

The error is on the Consumer's Property "ActivePeriod" (a SerialObject) Property "StartDate". 

I convert the JSON from Postman to DynamicObject and assign it to a new Consumer record, because the ActivePeriod is a Proeprty and StartDate again a property of that, I assign it as follows:

Set consumerRecord.ActivePeriod.StartDate = obj.activePeriod.startDate

The format of the JSON date is "2019-02-01". It seems to be happy with the date of the Proeprty "dateOfBirth" and "DeceasedDateTime", but they are not nested Properties.

There is no clear documention for InterSystems Caché how the Dynamic Object handles nested Properties and how to retrieve it, I cannot see anything wrong in my code.

 

Here is the full code listing and following that the JSON I sent in Postman:

Postman JSON:

---------------------

{        
        "familyName":  "Smith",
        "firstNames" : "Peter",
        "dateOfBirth": "1978-10-10",
        "isDateOfBirthApproximate": 0,
        "isDeceased":  false,
        "deceasedDateTime": "",
        "activePeriod": {
            "startDate": "2019-02-01",
            "endDate":   "",
            "isActive":  1,
            "reasonChangedOrDeleted":  ""
        },
        "am": {
            "createdBy": "Elize",
            "createdDateTime": "2019-02-22 03:34:00",
            "modifiedBy": "Elize",
            "modifiedDateTime": "2019-02-22 03:34:00"
        }

}

Comments

Elize VdRiet · Feb 24, 2019

Apologies, I forgot to add the REST service code, the POST "AddConsumer" uses "CopyToConsumerFromJSON" and this is the one where the de-serialized dynamic object is assigned to the Consumer record:

 

Spoiler

Class REST.ConsumerRestService Extends %CSP.REST
{
    ///============================================================================================
    /// UrlMap
    ///============================================================================================
    XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
    {
    <Routes>
        <!-- Return the list of all Consumers -->
        <Route Url="/consumers" Method="GET" Call="GetAllConsumers" />
        <!-- Add a new Consumer -->
        <Route Url="/consumer" Method="POST" Call="AddConsumer" />
        <!-- Return the details for a single Consumer -->
        <Route Url="/consumer/:id" Method="GET" Call="GetConsumer" />
        <!-- Update the details for the specified Consumer -->
        <Route Url="/consumer/:id" Method="PUT" Call="UpdateConsumer" />
        <!-- Delete the specified Consumer -->
        <Route Url="/consumer/:id" Method="DELETE" Call="DeleteConsumer" />
    </Routes>
    }
    
    ///--------------------------------------------------------------------------------------------
    /// Return the list of all Consumers
    ///--------------------------------------------------------------------------------------------
    ClassMethod GetAllConsumers() As %Status 
    {
        Set tArr = []
        Set rs = ##class(%SQL.Statement).%ExecDirect(,"SELECT * FROM Consumer.Consumer")
        While rs.%Next() {
            Do tArr.%Push({
                "familyName":  (rs.%Get("FamilyName")),
                "firstNames" : (rs.%Get("FirstNames")),
                "dateOfBirth": (rs.%Get("DateOfBirth")),
                "isDateOfBirthApproximate": (rs.%Get("IsDateOfBirthApproximate")),
                "isDeceased":  (rs.%Get("IsDeceased")),
                "deceasedDateTime": (rs.%Get("DeceasedDateTime")),
                "activePeriod": {
                    "startDate": (rs.%Get("ActivePeriod.StartDate")),
                    "endDate":   (rs.%Get("ActivePeriod.EndDate")),
                    "isActive":  (rs.%Get("ActivePeriod.IsActive")),
                    "reasonChangedOrDeleted":  (rs.%Get("ActivePeriod.ReasonChangedOrDeleted"))
                },
                "am": {
                    "createdBy": (rs.%Get("AM.CreatedBy")),
                    "createdDateTime": (rs.%Get("AM.CreatedDateTime")),
                    "modifiedBy": (rs.%Get("AM.ModifiedBy")),
                    "modifiedDateTime": (rs.%Get("AM.ModifiedDateTime"))
                }
            })
        }
        
        Write tArr.%ToJSON()
        Quit $$$OK
    }
    
    ///--------------------------------------------------------------------------------------------
    /// Get record for a Consumer
    ///--------------------------------------------------------------------------------------------
    ClassMethod GetConsumer(id As %Integer = 0) As %Status 
    {
        Set consumer = ##class(Consumer.Consumer).%OpenId(id) 
        
        If '$IsObject(consumer) {
            Set %response.Status = ..#HTTP204NOCONTENT
        } Else {
            Set obj = {
                "familyName":  (consumer.FamilyName),
                "firstNames" : (consumer.FirstNames),
                "dateOfBirth": (consumer.DateOfBirth),
                "isDateOfBirthApproximate": (consumer.IsDateOfBirthApproximate),
                "isDeceased":  (consumer.IsDeceased),
                "deceasedDateTime": (consumer.DeceasedDateTime),
                "activePeriod": {
                    "startDate": (consumer.ActivePeriod.StartDate),
                    "endDate":   (consumer.ActivePeriod.EndDate),
                    "isActive":  (consumer.ActivePeriod.IsActive),
                    "reasonChangedOrDeleted":  (consumer.ActivePeriod.ReasonChangedOrDeleted)
                },
                "am": {
                    "createdBy": (consumer.AM.CreatedBy),
                    "createdDateTime": (consumer.AM.CreatedDateTime),
                    "modifiedBy": (consumer.AM.ModifiedBy),
                    "modifiedDateTime": (consumer.AM.ModifiedDateTime)
                }
            }
            
            Write obj.%ToJSON()
            
        }
        
        Quit $$$OK
    }
    
    ///--------------------------------------------------------------------------------------------
    /// Update the details for the specified Consumer
    ///--------------------------------------------------------------------------------------------
    ClassMethod UpdateConsumer(id As %Integer = 0) As %Status 
    {
        If '..GetJSONFromRequest(.obj) {
            Set %response.Status = ..#HTTP400BADREQUEST
            Set error = {"errormessage": "JSON not found"}
            Write error.%ToJSON()
            Quit $$$OK
        }
        
        If '..ValidateJSON(obj,.error) {
            Set %response.Status = ..#HTTP400BADREQUEST
            Write error.%ToJSON()
            Quit $$$OK
        }
        
        Set consumerRecord = ##class(Consumer.Consumer).%OpenId(id)
        If consumerRecord = "" {
            Set %response.Status = ..#HTTP404NOTFOUND
            Set error = {"errormessage": "Consumer does not exist"}
            Write error.%ToJSON()
            Quit $$$OK
        }
        
        Do ..CopyToConsumerFromJSON(.consumerRecord,obj)
        
        Set sc = consumerRecord.%Save()
        
        Set result={}
        Do result.%Set("Status",$s($$$ISERR(sc):$system.Status.GetOneErrorText(sc),1:"OK"))
        Write result.%ToJSON()
        
        Quit sc
    }

    ///--------------------------------------------------------------------------------------------
    /// Add a new Consumer
    ///--------------------------------------------------------------------------------------------
    ClassMethod AddConsumer(id As %Integer = 0) As %Status 
    {
    
        If '..GetJSONFromRequest(.obj) {
            Set %response.Status = ..#HTTP400BADREQUEST
            Set error = {"errormessage": "JSON not found"}
            Write error.%ToJSON()
            Quit $$$OK
        }
        
        If '..ValidateJSON(obj,.error) {
            Set %response.Status = ..#HTTP400BADREQUEST
            Write error.%ToJSON()
            Quit $$$OK
        }
        
        Set consumerNew = ##class(Consumer.Consumer).%New()

        Do ..CopyToConsumerFromJSON(.consumerNew, obj)
        
        Set sc = consumerNew.%Save()
        
        Set result = {}
        do result.%Set("Status",$s($$$ISERR(sc):$system.Status.GetOneErrorText(sc),1:"OK"))
        write result.%ToJSON()
        Quit sc
    }

    ///--------------------------------------------------------------------------------------------
    /// Delete a Consumer
    ///--------------------------------------------------------------------------------------------
    ClassMethod DeleteConsumer(id As %Integer = 0) As %Status 
    {
        Set result={}
        Set sc=0
        
        If id '= "", ##class(Consumer.Consumer).%ExistsId(id) {
          Set sc = ##class(Consumer.Consumer).%DeleteId(id)
          Do result.%Set("Status",$s($$$ISERR(sc):$system.Status.GetOneErrorText(sc),1:"OK"))
        } Else  {
          Do result.%Set("Status","")    
        }
        
        Write result.%ToJSON()
        
          Quit sc
    }
    
    ///--------------------------------------------------------------------------------------------
    /// Helper method
    ///--------------------------------------------------------------------------------------------
    ClassMethod CopyToConsumerFromJSON(ByRef consumerRecord As Consumer.Consumer, obj As %DynamicObject) [ Private ]
    {
        
        Set consumerRecord.FamilyName  = obj.familyName
        Set consumerRecord.FirstNames  = obj.firstNames
        Set consumerRecord.DateOfBirth = obj.dateOfBirth
        Set consumerRecord.IsDateOfBirthApproximate = obj.isDateOfBirthApproximate
        Set consumerRecord.IsDeceased  = obj.isDeceased
        Set consumerRecord.DeceasedDateTime = obj.deceasedDateTime
        Set consumerRecord.ActivePeriod.StartDate = obj.activePeriod.startDate
        Set consumerRecord.ActivePeriod.EndDate = obj.activePeriod.endDate
        Set consumerRecord.ActivePeriod.IsActive = obj.activePeriod.isActive
        Set consumerRecord.ActivePeriod.ReasonChangedOrDeleted = obj.activePeriod.reasonChangedOrDeleted
        Set consumerRecord.AM.CreatedBy = obj.am.createdBy
        Set consumerRecord.AM.CreatedDateTime = obj.am.createdDateTime
        Set consumerRecord.AM.ModifiedBy = obj.am.modifiedBy
        Set consumerRecord.AM.ModifiedDateTime = obj.am.modifiedDateTime
    }
    
    /// Helper method
    ClassMethod GetJSONFromRequest(Output obj As %DynamicObject) As %Boolean
    {
        Set ok = 1
        Try {
            Set jsonData = %request.Content.Read()
            Set obj = ##class(%DynamicObject).%FromJSON(jsonData)
        } Catch ex {
            Set ok = 0
        }
        Quit ok
    }
    
    ///--------------------------------------------------------------------------------------------
    /// Helper method
    ///--------------------------------------------------------------------------------------------
    ClassMethod ValidateJSON(obj As %DynamicObject, Output error As %DynamicObject) As %Boolean
    {
        Set error = {}
        
        If obj.%Get("familyName") = "" {
            Set error.errormessage = "familyName was blank"
            Quit 0
        }    
        
        If obj.%Get("firstNames") = "" {
            Set error.errormessage = "firstNames was blank"
            Quit 0
        }
        
        Quit 1
    }
    
}

 

0
Elize VdRiet  Feb 24, 2019 to Eduard Lebedyuk

Thank you for posting Eduard, you are absolutely right, I scratched around and was just about to post here the solution I found, but it is the same as you are suggesting, so I sovled it by covnerting the date sting to Caché date format with:

Set consumerRecord.ActivePeriod.StartDate = $ZDATEH(obj.activePeriod.startDate, 15)

But the IsValid check I did not know about and will be very handy, because yes, if the date is empty string or not valid the above conversion threw an error as well and I do not want an error since the date may be empty, so I have to test it first, so will add that now. And yes, we are using IRIS, so the %PosicTime tip is handy too.

0
Eduard Lebedyuk · Feb 24, 2019

You get this error (Datatype value '2019-02-01' is not a valid number) because %Date stores data in horolog format, so you need to do one of:

  • (Recommended approach) Convert date value from external format (2019-02-01) into internal format (horolog) using $zdh function:
Set consumerRecord.ActivePeriod.StartDate = $zdh(obj.activePeriod.startDate, 3)
  • Use datatype for which this value is valid, i.e. %TimeStamp. To check, all datatype classes offer IsValid method, you can use it to check value validity
zw ##class(%TimeStamp).IsValid("2019-02-01")
  • If you're using InterSystems IRIS you can store dates as %PosixTime.

Regarding Atelier, I use it to debug REST services and it shows variable values. You can try to reinstall it. If you're on Windows you can also use Studio.

0
Evgeny Shvarov · Feb 24, 2019

Hi @Elize VdRiet !

Have you tried VSCode ObjectScript plugin already?  It doesn't have a debugger (yet), but it's lite-weight and cross-platform and has some nice features for ObjectScript support. 

0
Nicole Aaron · Feb 25, 2019

Elize - I'm sorry to hear that your Atelier experience has been frustrating. I've been able to use the Atelier debugger to step through code and see variable values without any problems, and it sounds like Eduard has as well.

InterSystems provides some resources that may be helpful to you:

  1. A video on using the Atelier debugger can be found here
  2. The Atelier documentation on using the debugger is here
  3. You can always reach out to InterSystems Support to work directly with an advisor (call +1 617-621-0700 or email support@intersystems.com)
0