This is the fourth page to this blog. Page 3 was a blog on how to parse the XML response. At the time, I didn't know how to parse json. Honestly, I really didn't understand JSON vs XML.
Concurrent with this project, I needed to convert a FedEx Rate and Time request. A little background. Currently I have a function in Foxpro that gets from Fedex the rate for shipping a package.
I use a SOAP request which is fairly simple. I run a web page on FedEx's web site passing in the URL everything about the shipment. It returns an XML stream that I parse.
FedEx is discontinuing this service as of June 2026. Instead, they are requiring an OAUTH authentication and it is only in JSON. No XML. So I was forced to learn to parse JSON. To do this, I asked ChatGPT to
explain the difference and give me a simple parsing routine to extract one value from the stream in Foxpro 6.0 code. In looking at this simple parsing routing, I understood the principles
and I realized it would be very easy to provide an option to parse this project in JSON.
What I also realized is that each entity (In QBO, an entitiy is a type of transaction such as Invoice or Bill) would require a separate parsing routine as you will see.
Lets get started. I called this program DBFIT1JSON.prg It was my first attempt.
So this first piece of code is a revised version to the MAKECALL.PRG at the bottom.
IF lohttp.STATUS = 200
LOCAL json, success, root, customers, i, CUST
lcxml=STRTRAN(lcresponse,'"',"")
IF mxml
DO dbfit6xml WITH mfilename,mchildfilename
ELSE
DO CASE
CASE mentity="Invoice" OR mentity="CreditMemo"
DO dbfit1json WITH mfilename,mchildfilename
CASE mentity="Bill" OR mentity="CreditMemo"
DO dbfit2json WITH mfilename,mchildfilename
ENDCASE
ENDIF
ENDIF
As you can see, different parsers are called depending on the entity being parsed. But the good news is that they are easily
graftable.
Here is the first part of the PRG to parse a request for a stream for the invoice and credit memo entities. Note,
this also includes the child records. You are going to see a createobject function in this program. This creates a problem.
If the com object fails to load, it will hang foxpro. On the first call, it seems to open fine. But we will make successive
calls and what was happening is that on the second call, the com object was busy or non responsive and hung Foxpro so I moved this line of
code back to the GETINVOICE.prg and run it only once instead of each successive parsing routine. Once created, it can be reused over
and over again so that worked.
**the resulting dbf is passed here. The file has already been created. Whether
parsing in JSON or XML, the resulting file is the same
PARAMETER lcdbf,lcchilddbf
**mentity is used to filter the fieldlist file. To watch for partial matches, I added a space at the end.
**in JSON, i am using the mentity to scan down the json. To do this, the added space needs to be removed
mentity=trim(mentity)
SET SAFETY OFF
CLOSE DATA
USE (lcdbf)
IF mchild
SELECT 0
USE (lcchilddbf)
ENDIF
* lcJson = your JSON string. this isn't really necessary, I could have stayed with lcresponse
lcjson = lcresponse
**a big word about this next line of code. This is a COM object*move just this
next line to the first prg in the project
loScript = CREATEOBJECT("MSScriptControl.ScriptControl")
loScript.language = "JScript"
* Inject JSON into JS context
loScript.addcode([var data = ] + lcjson + [;])
*Sometimes there is no counter of invoices or credit memos so we must count them in a different way
*This could be done on lcjson or lcresponse
lcText = lcresponse && replace with your variable holding the text
* Count "sparse" occurrences
**it seems as though you can count sparse occurances as every full transaction has a sparse indicator
**this is a really cool way of determining how many "sparse" there are in the stream
lnInvoiceCount = (LEN(lcText) - LEN(STRTRAN(lcText, "sparse", ""))) / LEN("sparse")
Now we are ready to loop through the stream
FOR lnInv = 0 TO lnInvoiceCount - 1
wait window str(lninv,5) nowait
SELECT (LCDBF)
APPEND BLANK
* Get invoice fields
REPLACE ID WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].Id")
REPLACE synctoken WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].SyncToken")
REPLACE metcretime WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].MetaData.CreateTime")
REPLACE metlastupd WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].MetaData.LastUpdatedTime")
REPLACE docnumber WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].DocNumber")
REPLACE txndate WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].TxnDate")
REPLACE totaltax WITH val(loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].TotalTax"))*mmultiplier
REPLACE duedate WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].DueDate")
REPLACE amount WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].TotalAmt")*mmultiplier
REPLACE Custname WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].CustomerRef.name")
REPLACE memo WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].CustomerMemo")
REPLACE projectref WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].ProjectRef")
REPLACE Bill_Line1 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].BillAddr.Line1")
REPLACE Bill_Line2 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].BillAddr.Line2")
REPLACE Bill_Line3 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].BillAddr.Line3")
REPLACE Bill_Line4 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].BillAddr.Line4")
REPLACE Bill_Line5 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].BillAddr.Line5")
REPLACE Bill_Line6 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].BillAddr.Line6")
REPLACE Bill_Line7 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].BillAddr.Line7")
REPLACE Bill_Line8 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].BillAddr.Line8")
REPLACE Ship_Line1 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].ShipAddr.Line1")
REPLACE Ship_Line2 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].ShipAddr.Line2")
REPLACE Ship_Line3 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].ShipAddr.Line3")
REPLACE Ship_Line4 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].ShipAddr.Line4")
REPLACE Ship_Line5 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].ShipAddr.Line5")
REPLACE Ship_Line6 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].ShipAddr.Line6")
REPLACE Ship_Line7 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].ShipAddr.Line7")
REPLACE Ship_Line8 WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].ShipAddr.Line8")
REPLACE Ship_City WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].ShipAddr.City")
REPLACE Ship_state WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].ShipAddr.CountrySubDivisionCode")
REPLACE Ship_zip WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].ShipAddr.PostalCode")
* --- Count lines in transaction ---
lnLineCount = loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].Line.length")
if mchild
* --- Loop through lines and extract if present ---
FOR lnL = 0 TO lnLineCount - 1
IF loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].Line["+allt(STR(lnL,3))+"].DetailType")="SalesItemLineDetail"
SELECT (lcchilddbf)
APPEND BLANK
REPLACE docnumber WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].DocNumber")
REPLACE ID WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].Line["+allt(STR(lnL,3))+"].Id")
REPLACE lineno WITH VAL(loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].Line["+allt(STR(lnL,3))+"].Lineno"))
REPLACE descrip WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].Line["+allt(STR(lnL,3))+"].Description")
**some item are unitprice * quantity, others are just price
mprice=loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].Line["+allt(STR(lnL,3))+"].SalesItemLineDetail.UnitPrice")
IF TYPE("mprice")="C"
**then look for price instead
mprice=loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].Line["+allt(STR(lnL,3))+"].SalesItemLineDetail.Price")
ENDIF
IF TYPE("mprice")="C"
mprice=val(mprice)
ENDIF
mqty=loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].Line["+allt(STR(lnL,3))+"].SalesItemLineDetail.Qty")
IF TYPE("mqty")="C"
mqty=1
ENDIF
REPLACE price WITH mprice
REPLACE QTY WITH mqty*mmultiplier
REPLACE extprice with price*qty
REPLACE item WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].Line["+allt(STR(lnL,3))+"].SalesItemLineDetail.ItemRef.Name")
REPLACE itemid WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].Line["+allt(STR(lnL,3))+"].SalesItemLineDetail.ItemRef.Value")
REPLACE account WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].Line["+allt(STR(lnL,3))+"].SalesItemLineDetail.ItemAccountRef.name")
REPLACE accountid WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].Line["+allt(STR(lnL,3))+"].SalesItemLineDetail.ItemAccountRef.Value")
REPLACE taxcode WITH loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].Line["+allt(STR(lnL,3))+"].SalesItemLineDetail.TaxCodeRef.Value")
ENDIF
ENDFOR
ENDIF
ENDFOR
That is the JSON Parser.
Some notes. It is important to be able to visualize what the json looks like with it's indentations. The indentations tell you
the depth needed. This example doesn't get that deep. I will create another one of these parsing routines for parsing a report
stream. One trick to visualizing it is to suspend the program at the beginning of the program. Then do _cliptext=lcresponse.
This will place the entire stream into the clipboard. Then go to Chatgpt and ask them to indent the JSON. Make sure your
lcresponse is just a few transactions. Chatgpt will tell you the stream is too long. Then copy and past this into word.
Change the font to courier (Fixed width font) so you can see the indent structure. Another trick is to set step on in the program and
try to watch some lines like loScript.Eval("data.QueryResponse."+mentity+"["+allt(STR(lnInv,3))+"].DocNumber") Start with this
loScript.Eval("data.QueryResponse.Invoice[0].DocNumber") This should give you the first invoice #. the [0] is the incrementer
Next note. Unlike the XML parser, there is no need to create the parse.dbf and figure out the levels. This is much more
elegant and actually simpler by far.
Well that is it. Extracting is one thing, writing is another. Intuit only accepts JSON to write so the process is
to construct a JSON. My next project.
If you found this helpful, send me an email Craigs@sobelsoftware.com
Craig Sobel