2

I'm building a VBA app that creates and modifies Wordpress website pages using resources scraped from the web. The Wordpress API returns a JSON file but there is no native support for parsing JSON in VBA so I imported VBA-JSON from GitHub. Here is the subroutine:

Sub Wordpress()

    '
    ' Wordpress API Test
    '
    Dim wpResp As Variant
    Dim sourceSheet As String
    Dim resourceURL As String
    sourceSheet = "Resources"
    resourceURL = Sheets(sourceSheet).Cells(6, 1)
    wpResp = getJSON(resourceURL + "/wp-json/wp/v2/posts")

End Sub

And the function it calls.

Function getJSON(link) As Object

    Dim response As String
    Dim json As Object
    On Error GoTo recovery
    Dim retryCount As Integer
    retryCount = 0
    Dim web As MSXML2.XMLHTTP60
    Set web = New MSXML2.XMLHTTP60

the_start:

    web.Open "GET", link, False, UserName, pw
    web.setRequestHeader "Content-type", "application/json"
    web.send
    response = web.responseText
    While web.readyState <> 4
        DoEvents
    Wend

    On Error GoTo 0

    Debug.Print link
    Debug.Print web.Status; "XMLHTTP status "; web.statusText; " at "; Time

    Set json = JsonConverter.ParseJson(response)

    'getJSON = json ' this line produces Object variable or With block variable not set error but I can deal with it later

    Exit Function

recovery:

    retryCount = retryCount + 1
    Debug.Print "Error number: " & Err.Number & " " & Err.Description & " Retry " & retryCount
    Application.StatusBar = "Error number: " & Err.Number & " " & Err.Description & " Retry " & retryCount
    If retryCount < 4 Then GoTo the_start Else Exit Function
End Function

This code returns an Object/Collection with 1 item that contains a Variant/Object/Dictionary with 24 items but I'm lost on how to access these items. Here is a screenshot:

enter image description here

If I use the immediate window to query ?json.count I get the correct result "1" but after about six hours of researching on the web and trying as many variants as I could find, I'm still stuck on how to access the other 24.

Here is the JSON:

[{"id":1,"date":"2018-06-22T18:13:00","date_gmt":"2018-06-22T22:13:00","guid":{"rendered":"http:\/\/mytestsite.org\/?p=1"},"modified":"2018-06-22T18:13:00","modified_gmt":"2018-06-22T22:13:00","slug":"hello-world","status":"publish","type":"post","link":"http:\/\/mytestsite.org\/hello-world\/","title":{"rendered":"Blog Post Title"},"content":{"rendered":"<p>What goes into a blog post? Helpful, industry-specific content that: 1) gives readers a useful takeaway, and 2) shows you&#8217;re an industry expert. <\/p>\n<p>Use your company&#8217;s blog posts to opine on current industry topics, humanize your company, and show how your products and services can help people.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>What goes into a blog post? Helpful, industry-specific content that: 1) gives readers a useful takeaway, and 2) shows you&#8217;re&hellip;<\/p>\n","protected":false},"author":1,"featured_media":212,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[1],"tags":[],"_links":{"self":[{"href":"http:\/\/mytestsite.org\/wp-json\/wp\/v2\/posts\/1"}],"collection":[{"href":"http:\/\/mytestsite.org\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/mytestsite.org\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/mytestsite.org\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/mytestsite.org\/wp-json\/wp\/v2\/comments?post=1"}],"version-history":[{"href":"http:\/\/mytestsite.org\/wp-json\/wp\/v2\/posts\/1\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"http:\/\/mytestsite.org\/wp-json\/wp\/v2\/media\/212"}],"wp:attachment":[{"href":"http:\/\/mytestsite.org\/wp-json\/wp\/v2\/media?parent=1"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/mytestsite.org\/wp-json\/wp\/v2\/categories?post=1"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/mytestsite.org\/wp-json\/wp\/v2\/tags?post=1"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}]

At the end of the day, I want to be able to spin up a few hundred pages of WP content extracted and collated from several internet sources and keep them up to date using this app. Further suggestions beyond the problem here would also be useful so long as we don't get outside of VBA.

omegastripes
  • 12,351
  • 4
  • 45
  • 96
jeromekjerome
  • 501
  • 1
  • 8
  • 26
  • Please share a working URL or post JSON sample. – omegastripes Jul 01 '18 at 17:46
  • Maybe I'm missing something but try `?JSON.items(1).items(1)` in immediate window or if you know the dictionary's keys in advance by name `?JSON("PutCollectionKeyNameHere")("PutDictionaryKeyNameHere")` – chillin Jul 01 '18 at 18:32
  • Also, just above your Exit Function, I think `getJSON = JSON` should be `Set getJSON = json` as you are assigning an object. – chillin Jul 01 '18 at 18:35
  • 1
    `?json(1).count` – omegastripes Jul 01 '18 at 18:47
  • Here is the JSON responseText – jeromekjerome Jul 01 '18 at 19:31
  • chillin, Set getJSON = json works but then a 450 error is produced in the calling sub "Wrong number of arguments or invalid property assignment" when the function is called. – jeromekjerome Jul 01 '18 at 19:44
  • chillin, neither ?JSON.items(1).items(1) nor ?Json.items(1).items(1) work in the immediate window. They produce runtime error 424 "Object required" – jeromekjerome Jul 01 '18 at 19:52
  • chillin ?Json("id") produces runtime error 5 "Invalid procedure call or argument" – jeromekjerome Jul 01 '18 at 19:54
  • chllin, ?json.items(1).items(1) produces error 438 "Object doesn't support this property or method – jeromekjerome Jul 01 '18 at 19:58
  • If you are running windows 32 bit os, there is a solution which doesn't depend on any external converter. Let me know If the version matches. – SIM Jul 01 '18 at 21:04
  • @SIM Why [only 32 bit](https://stackoverflow.com/a/38134477/2165759)? – omegastripes Jul 01 '18 at 21:32
  • You are an invaluable asset @omegastripes. I didn't know there is a way to play around with 64 bit as well. Thanks. – SIM Jul 01 '18 at 21:36
  • I am running 64bit Windows 7. Please don't suggest changing the JSON that comes from the Wordpress site. I don't control that. Since this is a single-case solution, parsing the JSON and constructing the JSON text for the POSt can be specific to the Wordpress specification and need not handle any other cases. – jeromekjerome Jul 01 '18 at 23:22
  • Before making any comment, It's better to go through the relevant context @TinMan. I'm not a big fan of `JsonConverter`. Thanks. – SIM Jul 02 '18 at 07:02
  • @SIM fair enough. I think that omegastripes's [JSON.Bas](https://github.com/omegastripes/VBA-JSON-parser) is brilliant. In truth, his post doesn't actually answer the OP's question. So maybe I was wrong for the upvote. – TinMan Jul 02 '18 at 08:43
  • You again got me wrong dear. I'm a great fan of omegastripes. Whatever he does I like it cause it contains something new to learn. What I don't like is hire any external converter to do the vba stuff. That's it. – SIM Jul 02 '18 at 09:39
  • @ omegastripes Now I need to make the round trip. Do you have a companion snippet that will reconstruct the unpacked JSON and pack it back up again? This has to be a much simpler task, since the contents are now known. – jeromekjerome Jul 02 '18 at 10:15
  • @Jerome If I'm understanding right, you need to convert JSON object into JSON string? If so then you may just use `JSON.Serialize()` function. – omegastripes Jul 02 '18 at 11:36
  • @ omegastripes The overall objective of the exercise is to query a Wordpress site (generically but only Wordpress, no other platforms, so the structure of the data is already well-defined), unpack the JSON to Excel (or Access), then modify the data (content of the Wordpress page), then repack the JSON and send it back to the site, thus updating the page. Now that your code unpacks it and drops it into a sheet(s), I just need to be able to take that same sheet(s) and repack the data in it to JSON the same way it was before you unpacked it. – jeromekjerome Jul 02 '18 at 15:28

2 Answers2

5

The JsonConverter is returning a collection of VBA.Collections Scripting.Dictionaries, and Values. In order to understand the output you will have to test the TypeName of all the returned values.

The real question is "How to navigate through a json object (or any unknown object for that matter) and access the values within.

Immediate Window

Using the Immediate Window and the json object from the OP's post I will try to describe the thought process (in the style of the must read book: The Little Schemer)

' What is json?
?TypeName(JSON)
Collection

'json is a collection
'How big is JSON
?JSON.Count
 1 

'JSON is a collection of 1 Item
'What is Type that Item?
?TypeName(JSON(1))
Dictionary

'JSON(1) is a Dictionary
'What is the first key in the JSON(1) Dictionary?
?JSON(1).Keys()(0)
id

'The first key in the JSON(1) Dictionary is "id"
'What is the Type of the value of "id"?
?TypeName(JSON(1)("id"))
Double

'JSON(1)("id") is a number
'What is its value
?JSON(1)("id")
 1 

Of course this process can get tedious consider the amount of nesting in this JSON Object.

JSON(1)("_links")("curies")(1)("templated")

Collection|Dictionary|Dictionary|Collection|Boolean Value

So I guess the best thing to do is write a function that will print all the accessor to the Immediate Window and go from there.

enter image description here

PrintJSONAccessors:Sub

Sub PrintJSONAccessors(JSON As Variant, Optional Prefix As String)
    Dim data As Variant, Key As Variant, Value As Variant
    Dim Accessor As String, ArrayAccessor As String
    Dim n As Long
    If TypeName(JSON) = "Collection" Then
        For n = 1 To JSON.Count
            Accessor = Prefix & "(" & n & ")"
            If TypeName(JSON(n)) = "Dictionary" Or TypeName(JSON(n)) = "Collection" Then
                PrintJSONAccessors JSON(n), Accessor
            Else
                Debug.Print Accessor
            End If
        Next
    Else
        For Each Key In JSON
            If TypeName(Key) = "Dictionary" Or TypeName(Key) = "Collection" Then
                PrintJSONAccessors Key, Prefix
            ElseIf TypeName(JSON(Key)) = "Dictionary" Or TypeName(JSON(Key)) = "Collection" Then
                Accessor = Prefix & "(" & Chr(34) & Key & Chr(34) & ")"
                PrintJSONAccessors JSON(Key), Accessor
            ElseIf TypeName(JSON(Key)) = "Dictionary" Then
                Accessor = Prefix & "(" & Chr(34) & Key & Chr(34) & ")"
                PrintJSONAccessors JSON(Key), Accessor
            ElseIf TypeName(JSON(Key)) = "Variant()" Then
                data = JSON(Key)
                For n = LBound(data) To UBound(data)
                    Accessor = Prefix & "(" & Chr(34) & Key & Chr(34) & ")"
                    ArrayAccessor = Prefix & "(" & Chr(34) & Key & Chr(34) & ")" & "(" & n & ")"
                    If TypeName(data(n)) = "Dictionary" Then
                        PrintJSONAccessors data(n), ArrayAccessor
                    Else
                        Debug.Print ArrayAccessor
                    End If
                Next
            Else
                Accessor = Prefix & "(" & Chr(34) & Key & Chr(34) & ")"
                Debug.Print Accessor
            End If
        Next
    End If
End Sub

Usage:

 PrintJSONAccessors JSON, "?JSON"

It appears that the MSScriptControl.ScriptControl only works on 32 bit systems. I guess that is what SIM was alluding to in his comments. Although, my answer is IMO correct, you should ignore the next section of comments.

FYI: I posted a function that parses the JSON into Arrays and Dictionaries Function to Return a JSON Like Objects Using VBA Collections and Arrays on Code Review. It is not a replacement for JsonConverter or omegastripes's JSON.Bas. It demonstrates that you can add JScript code to CreateObject("MSScriptControl.ScriptControl") and use it to process the JSON.

Community
  • 1
  • 1
TinMan
  • 6,624
  • 2
  • 10
  • 20
  • Nice. Does the MSScriptControl.ScriptControl work for 64 bit? I thought only for 32. Are there any risks associated with using ScriptControl? – QHarr Jul 02 '18 at 05:19
  • @QHarrI did not realize that. For whatever reason Office 365 refuses to install the 64Bit version of my System. Thanks for the info, Bro! – TinMan Jul 02 '18 at 09:10
  • i think you wrote an answer somewhere where you gave the mappings for json objects within jsonconverter.bas i.e. [] = collection etc..... I wanted to refer an OP to your answer but can't find it. Can you remember that answer by any change please? – QHarr Aug 20 '19 at 12:28
  • @QHarr Is this the one [How to get, JSON values to Work in VBA-JSON?](https://stackoverflow.com/a/53494208/9912714) `[How to get, JSON values to Work in VBA-JSON?](https://stackoverflow.com/a/53494208/9912714)` – TinMan Aug 20 '19 at 17:46
  • No but also useful – QHarr Aug 20 '19 at 17:50
3

Try the code:

    Set json = JsonConverter.ParseJson(s)
    For Each k In json(1)
        Debug.Print k & vbTab & json(1)(k)
    Next

UPDATE

Take a look at the below example. Import JSON.bas module into the VBA project for JSON processing.

Option Explicit

Sub Test()

    Dim sJSONString As String
    Dim vJSON
    Dim sState As String
    Dim aData()
    Dim aHeader()
    Dim vResult

    ' Read JSON sample from file C:\Test\sample.json
    sJSONString = ReadTextFile("C:\Test\sample.json", 0)
    ' Parse JSON sample
    JSON.Parse sJSONString, vJSON, sState
    If sState = "Error" Then
        MsgBox "Invalid JSON"
        End
    End If
    ' Get the 1st element from root [] array
    Set vJSON = vJSON(0)
    ' Convert raw JSON to 2d array and output to worksheet #1
    JSON.ToArray vJSON, aData, aHeader
    With Sheets(1)
        .Cells.Delete
        .Cells.WrapText = False
        OutputArray .Cells(1, 1), aHeader
        Output2DArray .Cells(2, 1), aData
        .Columns.AutoFit
    End With
    ' Flatten JSON
    JSON.Flatten vJSON, vResult
    ' Convert flattened JSON to 2d array and output to worksheet #2
    JSON.ToArray vResult, aData, aHeader
    With Sheets(2)
        .Cells.Delete
        .Cells.WrapText = False
        OutputArray .Cells(1, 1), aHeader
        Output2DArray .Cells(2, 1), aData
        .Columns.AutoFit
    End With
    MsgBox "Completed"

End Sub

Sub OutputArray(oDstRng As Range, aCells As Variant)

    With oDstRng
        .Parent.Select
        With .Resize(1, UBound(aCells) - LBound(aCells) + 1)
            .NumberFormat = "@"
            .Value = aCells
        End With
    End With

End Sub

Sub Output2DArray(oDstRng As Range, aCells As Variant)

    With oDstRng
        .Parent.Select
        With .Resize( _
                UBound(aCells, 1) - LBound(aCells, 1) + 1, _
                UBound(aCells, 2) - LBound(aCells, 2) + 1)
            .NumberFormat = "@"
            .Value = aCells
        End With
    End With

End Sub

Function ReadTextFile(sPath As String, lFormat As Long) As String

    ' lFormat -2 - System default, -1 - Unicode, 0 - ASCII
    With CreateObject("Scripting.FileSystemObject").OpenTextFile(sPath, 1, False, lFormat)
        ReadTextFile = ""
        If Not .AtEndOfStream Then ReadTextFile = .ReadAll
        .Close
    End With

End Function

BTW, the similar approach applied in other answers.

omegastripes
  • 12,351
  • 4
  • 45
  • 96
  • I dimensioned this as a Variable and it works for the first three items but at item 4 it returns 450 error "Wrong number of arguments or invalid property assignment." – jeromekjerome Jul 01 '18 at 19:26
  • @Jerome Need JSON sample for further insight and some description what is the exact data you need to access. – omegastripes Jul 01 '18 at 19:36
  • just posted JSON sample – jeromekjerome Jul 01 '18 at 19:58
  • @Jerome I added example to the answer. – omegastripes Jul 01 '18 at 20:45
  • I imported the .bas file and now it's in module all its own but I don't see any public functions, so I'm at a loss about how to use it. Nor could I find any documentation anywhere, no installation notes. How do I use it? Do I have to copy it into my own working module? – jeromekjerome Jul 02 '18 at 00:01
  • I copied the sample you provided above into my own module but now it produces a runtime error 9 subscript out of range on this line: sJSONString = "[" & Split(sJSONString, "[", 2)(1) – jeromekjerome Jul 02 '18 at 00:25
  • If I jump over that error, it runs to completion and produces a result that contains the expected data but I can't tell what it would do if that line had not failed since I don't understand your code. – jeromekjerome Jul 02 '18 at 00:33
  • 1
    Nice work on [JSON.Bas](https://github.com/omegastripes/VBA-JSON-parser). I also loved the way you scraped the post. +1∞ – TinMan Jul 02 '18 at 02:46
  • 1
    @Jerome It was intended that the code reads JSON sample directly from this post on SO. Seems something goes wrong on your PC, so I changed the code, and now you should save the JSON sample to a local file, see the comments in the code. – omegastripes Jul 02 '18 at 02:49
  • Everything good. I found the problem and tweaked the code a little. – jeromekjerome Jul 02 '18 at 10:12