Parse JSON with VBA (Access 2010) subscript out of range error - json

I'm parsing a JSON string similar to the solution at this link: Parse JSON with VBA (Access 2010). However, I'm getting the "subscript out of range" error.
Public Sub GetValues()
Dim s As String, rates(), i As Long
s = "{""id"":""14acfa60-c0e1-47fb-8f80-ca0831bf3b52"",""class"":""us_equity"",""exchange"":""ARCA"",""symbol"":""UVXY"",""name"":"""",""status"":""active"",""tradable"":true,""marginable"":true,""shortable"":false,""easy_to_borrow"":false}"
rates = Array("id", "class", "exchange", "symbol", "name", "status", "tradeable", "marginable", "shortable", "easy_to_borrow")
For i = LBound(rates) To UBound(rates)
Debug.Print rates(i) & ":" & GetRate(s, rates(i))
Next i
End Sub
Public Function GetRate(ByVal s As String, ByVal delimiter As String) As String
GetRate = Replace(Split(Split(s, delimiter & Chr$(34) & Chr$(58))(1), Chr$(44))(0), Chr$(125), vbNullString)
End Function

You have a typo in your code:
Public Sub GetValues()
Dim s As String, rates(), i As Long
'Just for better reading.
's = "{""id"":""14acfa60-c0e1-47fb-8f80-ca0831bf3b52"", _
""class"":""us_equity"", _
""exchange"":""ARCA"", _
""symbol"":""UVXY"", _
""name"":"""", _
""status"":""active"", _
""tradable"":true, _
""marginable"":true, _
""shortable"":false, _
""easy_to_borrow"":false}"
'""tradable"":true, _ <<<<< ERROR in s var. In your rate array you say: "tradeable"
' "tradeable", _ <<<<< rate Array! (I just change it to run the code)
s = "{""id"":""14acfa60-c0e1-47fb-8f80-ca0831bf3b52"",""class"":""us_equity"",""exchange"":""ARCA"",""symbol"":""UVXY"",""name"":"""",""status"":""active"",""tradable"":true,""marginable"":true,""shortable"":false,""easy_to_borrow"":false}"
rates = Array("id", _
"class", _
"exchange", _
"symbol", _
"name", _
"status", _
"tradable", _
"marginable", _
"shortable", _
"easy_to_borrow")
For i = LBound(rates) To UBound(rates)
Debug.Print rates(i) & ":" & GetRate(s, rates(i))
Next i
End Sub
Public Function GetRate(ByVal s As String, ByVal delimiter As String) As String
'Chr$(34) = "
'Chr$(58) = :
'Chr$(125) = }
'Again... better reading.
Dim A: A = Split(s, delimiter & Chr$(34) & Chr$(58))(1)
Dim B: B = Split(A, Chr$(44))(0)
Dim C: C = Chr$(125)
GetRate = Replace(B, C, vbNullString)
End Function

First of all the issue in your code is that you have a typo: In your JSON you have tradable but your rate is called tradeable.
I recommend to include a proper error handling in your function. So if something gets wrong there you don't get stuck but a error message instead.
I also recommend not to have everything in one line in your function like Replace(Split(Split(… because if something gets wrong you don't know in which part it went wrong: First or second Split or the Replace. So if you do that in multiple lines (see below) then you can return a more useful error message.
Shorter code is not necessarily faster and better. But code that is easily readable, debugable and maintainable is very good code because you will make less errors and find them quicker.
I highly recommend to use meaningful variable names. Names like s for example are very bad names. If you use Json instead you always immediately see that this variable contains your JSON string.
Meaningful variables make your code better because it is more human readable and VBA doesn't care about the extra 3 characters.
Finally I would declare variables as close as possible to their first use.
So the code below is a bit longer but has much more improved readability and an error handling that gives at least a proper info if the key word you were looking for did not exist in your JSON.
Option Explicit
Public Sub GetValues()
Dim Json As String
Json = "{""id"":""14acfa60-c0e1-47fb-8f80-ca0831bf3b52"",""class"":""us_equity"",""exchange"":""ARCA"",""symbol"":""UVXY"",""name"":"""",""status"":""active"",""tradable"":true,""marginable"":true,""shortable"":false,""easy_to_borrow"":false}"
Dim Rates() As Variant
Rates = Array("id", "tradeable", "class", "exchange", "symbol", "name", "status", "tradeable", "marginable", "shortable", "easy_to_borrow")
Dim i As Long
For i = LBound(Rates) To UBound(Rates)
Debug.Print Rates(i) & ":" & GetRate(Json, Rates(i))
Next i
End Sub
Public Function GetRate(ByVal Key As String, ByVal Delimiter As String) As String
On Error GoTo RETURN_ERR
Dim SplitKey() As String
SplitKey = Split(Key, Delimiter & Chr$(34) & Chr$(58))
If UBound(SplitKey) = 0 Then
GetRate = "KEY NOT FOUND"
Exit Function
End If
Dim ValueOfKey As String
ValueOfKey = Split(SplitKey(1), Chr$(44))(0)
'remove } from value
ValueOfKey = Replace(ValueOfKey, Chr$(125), vbNullString)
'return
GetRate = ValueOfKey
Exit Function
RETURN_ERR:
GetRate = "Unknown error while extracting value. Check the JSON syntax."
End Function

Related

VBA JSON - Parse Multiple Values

I'm having trouble using the JSON-VBA converter with a multiple values key.
I have the normal recursion routines written to navigate JSON trees but here's an example of a simple JSON parse which I can't seem to get to work.
See this for the VBA-JSON converter software, which is terrific and fast.
Environment: Windows 7 / Access 2016 / Private LAN (no Internet)
Here's the code:
Option Compare Database
Option Explicit
Sub testparse()
Dim js As String, i As Long, jo As Object, item As Variant
Dim keys(), vals()
' fails on this string
js = "{ !Category!: !Famous Pets!," & _
"!code!: [!a!,!b!,!c!] }" ' string with multiple values
' with the following string, this works
js = "{ !Category!: !Famous Pets!," & _
" !code!: !singlecodevalue! }"
js = Replace(js, "!", Chr(34)) ' replace ! with quotes
Debug.Print " js = " & js
Set jo = JsonConverter.ParseJson(js) ' returns object with json elements
i = 0
ReDim keys(1 To jo.Count)
ReDim vals(1 To jo.Count)
Debug.Print " Number keys found at top level " & jo.Count
For Each item In jo
i = i + 1
keys(i) = item
vals(i) = jo(item)
Next item
For i = 1 To jo.Count
Debug.Print "key " & keys(i) & " = " & vals(i)
Next i
End Sub
For each item you encounter when running through a JSON object, you have to determine what you're dealing with -- especially if you don't know ahead of time how many items in an array! It gets even trickier if you have a compound JSON structure with collections inside arrays and such.
The bottom line is that you have to check each item you pull out of the JSON object and figure out what it is before accessing it. The top level of a JSON object (assuming the use of JsonConverter) will always be a Dictionary. So you can count on looping through the keys of the top level dictionary:
Dim json As Dictionary
Set json = JsonConverter.ParseJson(someJsonString)
Dim topLevelKey As String
For Each topLevelKey In json
Dim item As Variant
Debug.Print topLevelKey & " = " & item
Next topLevelKey
The problem with this is the item is not always a simple string. It can be a value (String), an array (Collection), or a group (Dictionary). See this answer as a good reference.
Basically, this means you have to check each item before you use it. So you can check it like this:
Select Case TypeName(item)
Case "Collection"
'--- loop through the item as a Collection
Case "Dictionary"
'--- loop through the item as a Dictionary
Case Else
'--- the item is a value of some type (String, Boolean, etc)
End Select
In my example here, I created a sub called ParseItem that checks each of the items in this manner. Reworking your original code into the example below:
Option Explicit
Sub testparse()
Dim js As String, i As Long, jo As Object, item As Variant
Dim keys(), vals()
' fails on this string
js = "{ !Category!: !Famous Pets!," & _
"!code!: [!a!,!b!,!c!] }" ' string with multiple values
' with the following string, this works
' js = "{ !Category!: !Famous Pets!," & _
' " !code!: !singlecodevalue! }"
'--- compound example
' js = "{ !Category!: !Famous Pets!,!code!: [!a!,!b!,{!c! : { !c1! : !1!, !c2!:!2!}}] }"
js = Replace(js, "!", Chr(34)) ' replace ! with quotes
Debug.Print "----------------------"
Debug.Print "js = " & js
Set jo = JsonConverter.ParseJson(js) ' returns object with json elements
ParseDictionary 1, "root", jo
End Sub
Private Sub ParseCollection(ByVal level As Long, _
ByVal key As String, _
ByRef jsonCollection As Variant)
Dim item As Variant
For Each item In jsonCollection
ParseItem level, key, item
Next item
End Sub
Private Sub ParseDictionary(ByVal level As Long, _
ByVal key As String, _
ByRef jsonDictionary As Variant)
Dim dictKey As Variant
For Each dictKey In jsonDictionary
ParseItem level, dictKey, jsonDictionary(dictKey)
Next dictKey
End Sub
Private Sub ParseItem(ByVal level As Long, _
ByVal key As String, _
ByRef item As Variant)
Select Case TypeName(item)
Case "Collection"
Debug.Print Format(level + 1, "00 ") & key & " (collection)"
ParseCollection (level + 1), key, item
Case "Dictionary"
Debug.Print Format(level + 1, "00 ") & key & " (dictionary)"
ParseDictionary (level + 1), key, item
Case Else
Debug.Print Format(level, "00 ") & key & " = " & item
End Select
End Sub

VBA returning a value and error information together

I am writing some VBA in MS Access, although the principle of my question would apply just as well to Excel or Word VBA. I have written a function GetStringParameterFromTable which returns a string value. It is possible that the function may result in a VBA-generated error, despite my best efforts to write it so that it does not. If an error happens, I don't want the code to crash, so I must use error handling. However, I don't want the code to display an error message and stop within the function if there is an error. I want the function to finish executing and return control to the calling procedure, and then I want the calling procedure to display the error message and tidy up, e.g. close open files. My question is: how does the calling procedure know that there has been an error in the function it called, and how does it get the error message?
I have thought of three ways of implementing this:
(1) Make GetStringParameterFromTable into a Sub, and pass it ParameterValue, ErrorFlag and ErrorMessage by reference.
(2) Keep GetStringParameterFromTable as a Function, define ErrorFlag and ErrorMessage as global variables and have the function alter ErrorFlag and ErrorMessage.
(3) Keep GetStringParameterFromTable as a Function and define a type with three components – ParameterValue, ErrorFlag and ErrorMessage – and make GetStringParameterFromTable return a value of the type I have defined.
I think that my requirement must be quite common, but I can’t find any examples of how it’s implemented. Does anyone have any views on which of my suggestions is the best way, or whether there is a better way that I haven’t thought of?
I have been contemplating the same thing since C#.net has implemented Tuples. I have implemented Tuples using VBA's type to create my tuples. What I have done is the following:
Public Type myTuple
Value as String 'Or whatever type your value needs to be
ErrCode as Long
ErrDesc as String
End Type
Public Function DoWork (ByRef mObject as MyClass) as myTuple
Dim retVal as myTuple
'Do whatever work
If Err.Number <> 0 then
retVal.Value = Nothing
retVal.ErrNumber = Err.Number
retVal.ErrDesc = Err.Description
Else
Set retVal.Value = Whatever Makes Sense
retVal.ErrNumber = 0
retVal.ErrDesc = VbNullString
End If
DoWork = retVal
End Function
I would like to be more specific, but you didn't provide a code example.
I am doing it like this and log the errors in a table:
' Lookups Replacements
'---------------------
Function DLook(Expression As String, Domain As String, Optional Criteria) As Variant
On Error GoTo Err_Handler
Dim strSQL As String
strSQL = "SELECT " & Expression & " FROM " & Domain 'DLookup
'DCount: strSQL = "SELECT COUNT(" & Expression & ") FROM " & Domain
'DMax: strSQL = "SELECT MAX(" & Expression & ") FROM " & Domain
'DMin: strSQL = "SELECT SUM(" & Expression & ") FROM " & Domain
'DFirst: strSQL = "SELECT FIRST(" & Expression & ") FROM " & Domain
'DLast: strSQL = "SELECT LAST(" & Expression & ") FROM " & Domain
'DSum: strSQL = "SELECT SUM(" & Expression & ") FROM " & Domain
'DAvg: strSQL = "SELECT AVG(" & Expression & ") FROM " & Domain
If Not IsMissing(Criteria) Then strSQL = strSQL & " WHERE " & Criteria
DLook = DBEngine(0)(0).OpenRecordset(strSQL, dbOpenForwardOnly)(0)
Exit Function
Err_Handler:
'Can be made as Error Sub as well
Dim ErrNumber as Integer
Dim ErrDescription as String
ErrNumber = Err.Number
ErrDescription = Err.Description
Err.Clear
On Error Resume Next
Dim strSQL as String
strSQL = "INSERT INTO tblErrorLog (ErrorNumber, ErrorDescription) VALUES (" & ErrNumber & ", '" & ErrDescription & "')"
Currentdb.Excecute strSQL, dbFailOnError
End Function
Called with:
If DLook("Column2", "Table1", "Column1 = " & ID) = 0 Then
'Do stuff
End If
If DLook("Column2", "Table1") = 0 Then
'Do other stuff
End If

How to set a reference to a running object in Access VBA

I try to open a form in another database by using GetObject. Unfortunately I have to open a second instance of the database, but I would like to use the active instance of that database instead (if loaded). TO accomplish this I need to set an object reference to the running instance of that db.
What I currently use is the function below. This function first tries to activate the running instance of the database using its screen name, and if this generates an error the database and the form are loaded. However, if the database is already loaded I want to be able to load the form as well.
On lesser problem is if the error procedure to load the db and form generates an error, the error routine is not followed. How should I manage that?
Anyone has an idea?
I'm Using Access 2016
Thx.
Peter
Public Function AppDbOpen(strAppExec As String, strAppName As String, strOpenForm As String) As Boolean
On Error GoTo Err_Proc
Dim objDb As Object
'Activate DB if open
AppActivate strAppName
AppDbOpen = True
Exit_Err_Proc:
Set objDb = Nothing
Exit Function
Err_Proc:
Select Case Err.Number
Case 5 'Open Db if not open
Set objDb = GetObject(strAppExec, "Access.Application")
If Nz(strOpenForm, "") <> "" Then
objDb.DoCmd.OpenForm strOpenForm
End If
AppDbOpen = True
Case Else
MsgBox "Error: " & Trim(Str(Err.Number)) & vbCrLf & _
"Desc: " & Err.description & vbCrLf & vbCrLf & _
"Module: Mod_GeneralFunctions" & vbCrLf & _
"Function: AppDbOpen", _
vbCritical, "Error!"
End Select
Resume Exit_Err_Proc
End Function
This is not an easy task, but it can be accomplished by using some WinAPI window functions.
Essentially, you want to get an Access Application object by using the window title.
I'm going to assume you haven't got any unicode characters in that window title, else, we'll need something a little more complex.
First, declare our WinAPI functions:
Declare PtrSafe Function FindWindowExA Lib "user32" (Optional ByVal hWndParent As LongPtr, Optional ByVal hwndChildAfter As LongPtr, Optional ByVal lpszClass As String, Optional ByVal lpszWindow As String) As LongPtr
Declare PtrSafe Function AccessibleObjectFromWindow Lib "oleacc" (ByVal hWnd As LongPtr, ByVal dwId As Long, riid As Any, ppvObject As Object) As Long
FindWindowExA is used to find the window with the specified title. AccessibleObjectFromWindow is used to get the COM object of that window.
Then, we declare some constants to be used for AccessibleObjectFromWindow:
Const strIID_IDispatch As String = "{00020400-0000-0000-C000-000000000046}" 'To identify the IDISPATCH COM interface
Const OBJID_NATIVEOM As Long = &HFFFFFFF0 'To identify the object type
Then, we can write the function
Public Function AppDbOpen(strAppExec As String, strAppName As String, strOpenForm As String) As Boolean
On Error GoTo Err_Proc
Dim objDb As Object
'Activate DB if open
AppActivate strAppName
AppDbOpen = True
Dim hwndAppDb As LongPtr
hwndAppDb = FindWindowExA (,,,strAppName) 'Find the window handle (hWnd)
If hwndAppDb <> 0 Then 'If it's 0, something went wrong, check the title
Dim guid() As Byte
guid = Application.GuidFromString(strIID_IDispatch)
'Get the IDispatch object associated with that handle
AccessibleObjectFromWindow hwndAppDb, OBJID_NATIVEOM, guid(0), objDb
End If
If Nz(strOpenForm, "") <> "" Then
objDb.DoCmd.OpenForm strOpenForm
End If
Exit_Err_Proc:
Set objDb = Nothing
Exit Function
Err_Proc:
Select Case Err.Number
Case 5 'Open Db if not open
Set objDb = GetObject(strAppExec, "Access.Application")
If Nz(strOpenForm, "") <> "" Then
objDb.DoCmd.OpenForm strOpenForm
End If
AppDbOpen = True
Case Else
MsgBox "Error: " & Trim(Str(Err.Number)) & vbCrLf & _
"Desc: " & Err.description & vbCrLf & vbCrLf & _
"Module: Mod_GeneralFunctions" & vbCrLf & _
"Function: AppDbOpen", _
vbCritical, "Error!"
End Select
Resume Exit_Err_Proc
End Function
I'm not going to discuss the point of chained error handlers, but you can just review this answer for that. Note that resetting the error handler resets the Err object as well, so you might first want to store error number and description if you want to use that.
This worked like a charm, thank you so much! I never figured this out by myself.
It seems that after an adjustment of the code there is no issue related to the nested errors too. I needed to add a maximize call because mu forms are showed related to the screen and this causes an invisible form when the other database was minimized. The final code is now
Option Compare Database
Option Explicit
Declare PtrSafe Function FindWindowExA Lib "user32" (Optional ByVal hWndParent As LongPtr, _
Optional ByVal hwndChildAfter As LongPtr, Optional ByVal lpszClass As String, _
Optional ByVal lpszWindow As String) As LongPtr
Declare PtrSafe Function AccessibleObjectFromWindow Lib "oleacc" (ByVal hWnd As LongPtr, _
ByVal dwId As Long, riid As Any, ppvObject As Object) As Long
Const strIID_IDispatch As String = "{00020400-0000-0000-C000-000000000046}" 'To identify the IDISPATCH COM interface
Const OBJID_NATIVEOM = &HFFFFFFF0 'To identify the object type
Public Function AppDbOpen(strAppExec As String, strAppName As String, strOpenForm As String) As Boolean
On Error GoTo Err_Proc
Dim objDb As Object
Dim hwndAppDb As LongPtr
'Find the Db handle
hwndAppDb = FindWindowExA(, , , strAppName) 'Find the window handle (hWnd)
If hwndAppDb <> 0 Then 'If it's 0, something went wrong, check the title
'Activate DB if open
Dim guid() As Byte
guid = Application.GUIDFromString(strIID_IDispatch)
'Get the IDispatch object associated with that handle
AccessibleObjectFromWindow hwndAppDb, OBJID_NATIVEOM, guid(0), objDb
Else
'Open Db if not open
Set objDb = GetObject(strAppExec, "Access.Application")
End If
If Nz(strOpenForm, "") <> "" Then
objDb.RunCommand acCmdAppMaximize
objDb.DoCmd.OpenForm strOpenForm
objDb.Run "CenterForm", strOpenForm, False, False, False, 0
End If
AppDbOpen = True
Exit_Err_Proc:
Set objDb = Nothing
Exit Function
Err_Proc:
Select Case Err.Number
Case Else
MsgBox "Error: " & Trim(Str(Err.Number)) & vbCrLf & _
"Desc: " & Err.description & vbCrLf & vbCrLf & _
"Module: Mod_OpenExtDb" & vbCrLf & _
"Function: AppDbOpen", _
vbCritical, "Error!"
End Select
Resume Exit_Err_Proc
End Function
Again, thank you!
Peter

Serializing from object to JSON

I'm consuming a web service in some legacy applications written in VB6. Right now I've been able to parse the JSON returned from a web service using the VB JSON parser found here: http://www.ediy.co.nz/vbjson-json-parser-library-in-vb6-xidc55680.html
However, I'm still hardcoding the JSON string that gets passed into the POST request payload.
Generically speaking:
result = WebRequestPost(url, "{""Id"":""" & productId & """,""Name"":""" & productName & """,""Category"":""" & productCat & """,""Price"":""" & productPrice & """}")
Is there a cleaner way that I can generate a JSON payload based on an object?
I ended up building my own assembler of sorts...
Dim jsonArray() As String
'_______________________________________________________________
'Initializes the opening and closing braces of the JSON payload
Public Sub JSONInitialize()
ReDim jsonArray(1)
jsonArray(0) = "{"
jsonArray(1) = "}"
End Sub
'_______________________________________________________________
'Adds a string value to the JSON payload
Public Sub JSONAddString(nFieldName As String, nValue As String)
Dim temp As String
temp = jsonArray(UBound(jsonArray))
Dim index As Integer
index = UBound(jsonArray)
ReDim Preserve jsonArray(UBound(jsonArray) + 1)
jsonArray(UBound(jsonArray)) = temp
jsonArray(index) = """" & nFieldName & """:""" & nValue & ""","
End Sub
'_______________________________________________________________
'Adds an integer value to the JSON payload
Public Sub JSONAddInt(nFieldName As String, nValue As Integer)
Dim temp As String
temp = jsonArray(UBound(jsonArray))
Dim index As Integer
index = UBound(jsonArray)
ReDim Preserve jsonArray(UBound(jsonArray) + 1)
jsonArray(UBound(jsonArray)) = temp
jsonArray(index) = """" & nFieldName & """:" & nValue & ","
End Sub
So (sanitized) execution ends up looking like:
Dim o As New MyObject
Call o.JSONInitialize
Call o.JSONAddString("My JSON String Field", "Test String Value")
Call o.JSONAddInt("My JSON Int Field", 25)
o.JSONSerialize() returns:
{"My JSON String Field":"Test String Value","My JSON Int Field": 25,}
Unfortunately it puts the comma at the end so it won't win any beauty contests but the API I'm calling doesn't care.

Parse JSON with VBA (Access 2010)

I need to update a currency table in MS-Access with a JSON file below:
{
"timestamp": 1465843806,
"base": "CAD",
"rates": {
"AED": 2.87198141,
"AFN": 54.21812828,
"ALL": 95.86530071,
"AMD": 374.48549935,
"ANG": 1.39861507
}
}
The VBA code is as follows:
Private Sub cmdJsonTest_Click()
Set MyRequest = CreateObject("WinHttp.WinHttpRequest.5.1")
MyRequest.Open "GET", "https://website.org/api/latest.json?base=CAD"
MyRequest.send
' MsgBox MyRequest.ResponseText
Dim Json As Object
Set Json = JsonConverter.ParseJson(MyRequest.ResponseText)
MsgBox Json("base")
End Sub
The above code works correctly displaying a message box with CAD but I need to loop through and capture each currency code along with it's rate value. What syntax do I use to do this? I can provide the code for the function Json() function but did not see a way to upload it. Any assistance would be appreciated.
If you are using this json parser https://github.com/VBA-tools/VBA-JSON, use this code
Private Sub IterateDictionary(poDict As Dictionary)
Dim key As Variant
For Each key In poDict.Keys()
If TypeName(poDict(key)) = "Dictionary" Then
Debug.Print key
IterateDictionary poDict(key)
Else
Debug.Print key, poDict(key)
End If
Next
End Sub
EDIT:
You have to modify the debug.print with whatever process you want to do. To use this from your code put this line after MsgBox.
IterateDictionary Json
You could also string parse. For example, if after key pairs for the rates:
Option Explicit
Public Sub GetValues()
Dim s As String, rates(), i As Long
s = "{""timestamp"": 1465843806,""base"": ""CAD"",""rates"": {""AED"": 2.87198141,""AFN"": 54.21812828,""ALL"": 95.86530071,""AMD"": 374.48549935,""ANG"": 1.39861507}}"
rates = Array("AED", "AFN", "ALL", "AMD", "ANG")
For i = LBound(rates) To UBound(rates)
Debug.Print rates(i) & ":" & GetRate(s, rates(i))
Next i
End Sub
Public Function GetRate(ByVal s As String, ByVal delimiter As String) As String
GetRate = Replace(Split(Split(s, delimiter & Chr$(34) & Chr$(58))(1), Chr$(44))(0), Chr$(125), vbNullString)
End Function