VERSION 1.0 CLASS BEGIN MultiUse = -1 'True END Attribute VB_Name = "API_JSON" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = False Attribute VB_Exposed = False ' ======== JSON converter ======== ' Shared module version: 20220813 ' Tested in: TestCommons ' Depends on: ' Required reference: Scripting ' This code is based on rethinking VBA-JSON, https://github.com/VBA-tools/VBA-JSON Option Explicit Private Const ERR_PARSE_FAIL = vbObject + 2049 ' JSON states esaping '/' as optional. ' Use this flag to escape them as '\/' Public escapeSlash_ As Boolean ' JSON doesnt require escaping of non-AscII chars. ' Use this flag to enable \u codes escape mechanics Public escapeUnicode_ As Boolean Private multiline_ As Boolean Private wsCharacter_ As String Private wsCount_ As Long Private json_ As String Private inputLen_ As Long Private position_ As Long Private Sub Class_Initialize() escapeSlash_ = False escapeUnicode_ = False Call DisableMultiline End Sub ' {Integer|String} vWhitespace "Pretty" print json with given number of spaces per indentation (Integer) or given string Public Function SetupMultiline(ByVal vWhitespace As Variant) multiline_ = True If VBA.VarType(vWhitespace) = VBA.vbString Then wsCharacter_ = vWhitespace wsCount_ = -1 Else wsCharacter_ = vbNullString wsCount_ = vWhitespace End If End Function Public Function DisableMultiline() multiline_ = False wsCharacter_ = vbNullString wsCount_ = -1 End Function ' Convert object (Dictionary/Collection/Array) to JSON Public Function CreateJSON(ByRef vValue As Variant) As String CreateJSON = Generator_Process(vValue) End Function ' Convert JSON string to object (Dictionary/Collection) Public Function Parse(sJson$) As Object json_ = VBA.Replace(VBA.Replace(VBA.Replace(sJson, VBA.vbCr, ""), VBA.vbLf, ""), VBA.vbTab, "") position_ = 1 inputLen_ = VBA.Len(json_) Call Parser_SkipWS Select Case Parser_ViewAny() Case "{": Set Parse = Parser_Object() Case "[": Set Parse = Parser_Array() Case Else: Call Parser_RaiseError("Expected '{' or '['") End Select If position_ <= inputLen_ Then _ Call Parser_RaiseError("Some part of input is out of main object scope") json_ = vbNullString End Function Public Function SaveToFile(sFile$, iData As Object) As Boolean Const DO_OVERWRITE& = 2 SaveToFile = False On Error GoTo EXIT_ERROR Dim adoStream As Object: Set adoStream = CreateObject("ADODB.Stream") adoStream.Charset = "utf-8" Call adoStream.Open Call adoStream.WriteText(CreateJSON(iData)) Call adoStream.SaveToFile(sFile, DO_OVERWRITE) SaveToFile = True EXIT_ERROR: On Error GoTo 0 Call adoStream.Close End Function Public Function LoadFromFile(sFile$) As Object Dim sJson$: sJson = ReadFile(sFile) If sJson = vbNullString Then _ Exit Function Set LoadFromFile = Parse(sJson) End Function ' ============================================= Private Function ReadFile(sFile$) As String Dim adoStream As Object: Set adoStream = CreateObject("ADODB.Stream") adoStream.Charset = "utf-8" Call adoStream.Open On Error GoTo ERROR_FILE Call adoStream.LoadFromFile(sFile) On Error GoTo 0 ReadFile = adoStream.ReadText ERROR_FILE: Call adoStream.Close On Error GoTo 0 End Function Private Function Generator_Process(ByRef vValue As Variant, Optional nIdent& = 0) As String Select Case VBA.VarType(vValue) Case VBA.vbNull: Generator_Process = "null" Case VBA.vbBoolean: Generator_Process = IIf(vValue, "true", "false") Case VBA.vbDate: Generator_Process = """" & CDbl(vValue) & """" Case VBA.vbString If IsLargeNumber(vValue) Then Generator_Process = vValue ' Large number encoded as string Else Generator_Process = """" & EncodeStr(vValue) & """" End If Case VBA.vbArray To VBA.vbArray + VBA.vbByte: Generator_Process = Generator_Array(vValue, nIdent) Case VBA.vbObject If VBA.TypeName(vValue) = "Dictionary" Then Generator_Process = Generator_Dictionary(vValue, nIdent) ElseIf VBA.TypeName(vValue) = "Collection" Then Generator_Process = Generator_Collection(vValue, nIdent) Else ' TODO: call custom object function to generate JSON! Call Err.Raise(1000, Description:="Unknown object type. Please use Dictionary or Collection") End If Case VBA.vbInteger, VBA.vbLong, VBA.vbSingle, VBA.vbDouble, VBA.vbCurrency, VBA.vbDecimal Generator_Process = VBA.Replace(vValue, ",", ".") ' Number (use decimals for numbers) Case Else ' vbEmpty, vbError, vbDataObject, vbByte, vbUserDefinedType On Error Resume Next Generator_Process = vValue On Error GoTo 0 End Select End Function Private Function Generator_Array(ByRef vValue As Variant, nIdent&) As String Dim sBuffer$ Dim nBufferPosition& Dim nBufferLen& Call BufferAppend(sBuffer, "[", nBufferPosition, nBufferLen) Dim nSize1&: nSize1 = ArraySize(vValue, 1) If nSize1 <= 0 Then _ GoTo PRINT_TO_STR Dim nIndex1Start&: nIndex1Start = LBound(vValue, 1) Dim nSize2&: nSize2 = ArraySize(vValue, 2) Dim nIndex2Start& If nSize2 > 0 Then _ nIndex2Start = LBound(vValue, 2) Dim sIndentLevel1$: sIndentLevel1 = GetWS(nIdent + 1) Dim sIndentLevel2$: sIndentLevel2 = GetWS(nIdent + 2) Dim bIsFirst: bIsFirst = True Dim json_Converted$ Dim nIndex1& For nIndex1 = nIndex1Start To nIndex1Start + nSize1 - 1 Step 1 If Not bIsFirst Then Call BufferAppend(sBuffer, ",", nBufferPosition, nBufferLen) Else bIsFirst = False End If If nSize2 <= 0 Then ' 1D Array json_Converted = Generator_Nullable(vValue(nIndex1), nIdent + 1) If multiline_ Then _ json_Converted = vbNewLine & sIndentLevel1 & json_Converted Call BufferAppend(sBuffer, json_Converted, nBufferPosition, nBufferLen) Else ' 2D Array If multiline_ Then _ Call BufferAppend(sBuffer, vbNewLine, nBufferPosition, nBufferLen) Call BufferAppend(sBuffer, sIndentLevel1 & "[", nBufferPosition, nBufferLen) Dim nIndex2& Dim bIsFirst2 As Boolean: bIsFirst2 = True For nIndex2 = nIndex2Start To nIndex2Start + nSize2 - 1 Step 1 If Not bIsFirst2 Then Call BufferAppend(sBuffer, ",", nBufferPosition, nBufferLen) Else bIsFirst2 = False End If json_Converted = Generator_Nullable(vValue(nIndex1, nIndex2), nIdent + 2) If multiline_ Then _ json_Converted = vbNewLine & sIndentLevel2 & json_Converted Call BufferAppend(sBuffer, json_Converted, nBufferPosition, nBufferLen) Next nIndex2 If multiline_ Then _ Call BufferAppend(sBuffer, vbNewLine, nBufferPosition, nBufferLen) Call BufferAppend(sBuffer, sIndentLevel1 & "]", nBufferPosition, nBufferLen) End If Next nIndex1 PRINT_TO_STR: On Error GoTo 0 If multiline_ Then Call BufferAppend(sBuffer, vbNewLine & GetWS(nIdent) & "]", nBufferPosition, nBufferLen) Else Call BufferAppend(sBuffer, "]", nBufferPosition, nBufferLen) End If Generator_Array = BufferToString(sBuffer, nBufferPosition) End Function Private Function Generator_Collection(ByRef vValue As Variant, nIdent&) As String Dim sBuffer$ Dim nBufferPosition& Dim nBufferLen& Call BufferAppend(sBuffer, "[", nBufferPosition, nBufferLen) Dim bIsFirst As Boolean: bIsFirst = True Dim anElement As Variant For Each anElement In vValue If Not bIsFirst Then Call BufferAppend(sBuffer, ",", nBufferPosition, nBufferLen) Else bIsFirst = False End If Dim json_Converted$: json_Converted = Generator_Nullable(anElement, nIdent + 1) If multiline_ Then _ json_Converted = vbNewLine & GetWS(nIdent + 1) & json_Converted Call BufferAppend(sBuffer, json_Converted, nBufferPosition, nBufferLen) Next anElement If multiline_ Then Call BufferAppend(sBuffer, vbNewLine & GetWS(nIdent) & "]", nBufferPosition, nBufferLen) Else Call BufferAppend(sBuffer, "]", nBufferPosition, nBufferLen) End If Generator_Collection = BufferToString(sBuffer, nBufferPosition) End Function Private Function Generator_Dictionary(ByRef vValue As Variant, nIdent&) As String Dim sBuffer$ Dim nBufferPosition& Dim nBufferLen& Call BufferAppend(sBuffer, "{", nBufferPosition, nBufferLen) Dim bIsFirst As Boolean: bIsFirst = True Dim aKey As Variant For Each aKey In vValue.Keys Dim json_Converted$: json_Converted = Generator_Process(vValue(aKey), nIdent + 1) If json_Converted = vbNullString Then _ If IsUndefined(vValue(aKey)) Then _ GoTo NEXT_KEY ' For Objects, undefined (Empty/Nothing) is not added to object If Not bIsFirst Then Call BufferAppend(sBuffer, ",", nBufferPosition, nBufferLen) Else bIsFirst = False End If If multiline_ Then json_Converted = vbNewLine & GetWS(nIdent + 1) & """" & aKey & """: " & json_Converted Else json_Converted = """" & aKey & """:" & json_Converted End If Call BufferAppend(sBuffer, json_Converted, nBufferPosition, nBufferLen) NEXT_KEY: Next aKey If multiline_ Then Call BufferAppend(sBuffer, vbNewLine & GetWS(nIdent) & "}", nBufferPosition, nBufferLen) Else Call BufferAppend(sBuffer, "}", nBufferPosition, nBufferLen) End If Generator_Dictionary = BufferToString(sBuffer, nBufferPosition) End Function Private Function Generator_Nullable(ByVal vValue As Variant, nIdent&) Generator_Nullable = Generator_Process(vValue, nIdent) If Generator_Nullable = vbNullString Then _ If IsUndefined(vValue) Then _ Generator_Nullable = "null" End Function Private Function Parser_Object() As Scripting.Dictionary If Parser_ConsumeChar() <> "{" Then _ Call Parser_RaiseError("Expected '{'") Set Parser_Object = New Scripting.Dictionary Do Call Parser_SkipWS Dim sNextChar$: sNextChar = Parser_ViewChar() If sNextChar = "}" Then Call Parser_Increment Exit Function ElseIf sNextChar = "," Then Call Parser_Increment Call Parser_SkipWS End If Dim sKey$: sKey = Parser_Key() If Parser_ConsumeChar(1) <> ":" Then _ Call Parser_RaiseError("Expected ':'") sNextChar = Parser_ViewChar() If sNextChar = "[" Or sNextChar = "{" Then Set Parser_Object.Item(sKey) = Parser_Value() Else Parser_Object.Item(sKey) = Parser_Value() End If Loop End Function Private Function Parser_Array() As Collection If Parser_ConsumeChar() <> "[" Then _ Call Parser_RaiseError("Expected '['") Set Parser_Array = New Collection Do Call Parser_SkipWS Dim sChar$: sChar = Parser_ViewChar(1) If sChar = "]" Then Call Parser_Increment Exit Function ElseIf sChar = "," Then Call Parser_Increment End If Call Parser_Array.Add(Parser_Value()) Loop End Function Private Function Parser_Value() As Variant Call Parser_SkipWS Dim sValue$: sValue = Parser_ViewAny(1) Select Case sValue Case "{": Set Parser_Value = Parser_Object() Case "[": Set Parser_Value = Parser_Array() Case """", "'": Parser_Value = Parser_String() Case Else If VBA.InStr("+-0123456789", sValue) Then Parser_Value = Parser_Numbers() Exit Function End If If inputLen_ - position_ + 1 > 4 Then sValue = Parser_ViewAny(4) If sValue = "true" Then Parser_Value = True Call Parser_Increment(4) Exit Function ElseIf sValue = "null" Then Parser_Value = Null Call Parser_Increment(4) Exit Function End If End If If inputLen_ - position_ + 1 > 5 Then sValue = Parser_ViewAny(5) If sValue = "false" Then Parser_Value = False Call Parser_Increment(5) Exit Function End If End If Call Parser_RaiseError("Expected 'STRING', 'NUMBER', null, true, false, '{', or '['") End Select End Function Private Function Parser_String() As String Dim sBuffer$ Dim nBufferPosition& Dim nBufferLen& Dim sChar$ Dim json_Quote$: json_Quote = Parser_ConsumeChar() Do While position_ <= inputLen_ sChar = Parser_ConsumeAny(1) If sChar = json_Quote Then Parser_String = BufferToString(sBuffer, nBufferPosition) Exit Function ElseIf sChar = "\" Then ' Escaped string, \\, or \/ sChar = Parser_ConsumeAny(1) Select Case sChar Case """", "\", "/", "'": Call BufferAppend(sBuffer, sChar, nBufferPosition, nBufferLen) Case "b": Call BufferAppend(sBuffer, vbBack, nBufferPosition, nBufferLen) Case "f": Call BufferAppend(sBuffer, vbFormFeed, nBufferPosition, nBufferLen) Case "n": Call BufferAppend(sBuffer, vbCrLf, nBufferPosition, nBufferLen) Case "r": Call BufferAppend(sBuffer, vbCr, nBufferPosition, nBufferLen) Case "t": Call BufferAppend(sBuffer, vbTab, nBufferPosition, nBufferLen) Case "u" Dim sUTF8Char$: sUTF8Char = Parser_ViewAny(4) Call BufferAppend(sBuffer, VBA.ChrW(VBA.val("&h" + sUTF8Char)), nBufferPosition, nBufferLen) Call Parser_Increment(4) Case Else: Call Parser_Increment(-1) End Select Else Call BufferAppend(sBuffer, sChar, nBufferPosition, nBufferLen) End If Loop End Function Private Function Parser_Numbers() As Variant Call Parser_SkipWS Dim sChar$ Dim sValue$: sValue = vbNullString Do While position_ <= inputLen_ sChar = Parser_ViewAny(1) If VBA.InStr("+-0123456789.eE", sChar) Then sValue = sValue & sChar Call Parser_Increment(1) Else ' Fix: Parse -> String, Convert -> String longer than 15/16 characters containing only numbers and decimal points -> Number Dim bIsLarge As Boolean: bIsLarge = IIf(VBA.InStr(sValue, "."), VBA.Len(sValue) >= 17, VBA.Len(sValue) >= 16) If bIsLarge Then Parser_Numbers = sValue Else Parser_Numbers = VBA.val(sValue) End If Exit Function End If Loop End Function Private Function Parser_Key() As String Dim sChar$: sChar = Parser_ViewAny(1) If sChar = """" Or sChar = "'" Then Parser_Key = Parser_String Else Call Parser_RaiseError("Expected '""' or '''") End If End Function ' View nChars from input Private Function Parser_ViewAny(Optional nChars& = 1) As String If position_ + nChars - 1 > inputLen_ Then _ Call Parser_RaiseError("Unexpected end of input") Parser_ViewAny = VBA.Mid$(json_, position_, nChars) End Function ' View nChar symboms (skipping spaces) Private Function Parser_ViewChar(Optional nChars& = 1) As String Dim nPosition&: nPosition = position_ Do While nPosition > 0 And nPosition <= inputLen_ And VBA.Mid$(json_, nPosition, 1) = " " nPosition = nPosition + 1 Loop If nPosition + nChars - 1 > inputLen_ Then _ Call Parser_RaiseError("Unexpected end of input") Parser_ViewChar = VBA.Mid$(json_, nPosition, nChars) End Function ' Consumne nChars symbols from input Private Function Parser_ConsumeAny(Optional nChars& = 1) As String If position_ + nChars - 1 > inputLen_ Then _ Call Parser_RaiseError("Unexpected end of input") Parser_ConsumeAny = VBA.Mid$(json_, position_, nChars) Call Parser_Increment(nChars) End Function ' Consume nChars from input (skipping spaces) Private Function Parser_ConsumeChar(Optional nChars& = 1) As String Call Parser_SkipWS If position_ + nChars - 1 > inputLen_ Then _ Call Parser_RaiseError("Unexpected end of input") Parser_ConsumeChar = VBA.Mid$(json_, position_, nChars) Call Parser_Increment(nChars) End Function Private Function Parser_Increment(Optional nIncrement& = 1) position_ = position_ + nIncrement End Function Private Function Parser_SkipWS() Do While position_ <= inputLen_ And VBA.Mid$(json_, position_, 1) = " " Call Parser_Increment Loop End Function Private Function Parser_RaiseError(sMessage$) Dim nStart&: nStart = position_ - 10 If nStart <= 0 Then _ nStart = 1 Dim nEnd&: nEnd = position_ + 10 If nEnd > inputLen_ Then _ nEnd = inputLen_ Dim sErrorMsg$: sErrorMsg = "Error parsing JSON:" & VBA.vbNewLine & _ VBA.Mid$(json_, nStart, nEnd - nStart + 1) & VBA.vbNewLine & _ VBA.Space$(position_ - nStart) & "^" & VBA.vbNewLine & _ sMessage Call Err.Raise(ERR_PARSE_FAIL, "JSONConverter", sErrorMsg) End Function Private Function ArraySize(target As Variant, Optional nDimension% = 1) As Long On Error GoTo INVALID_SIZE ArraySize = UBound(target, nDimension) - LBound(target, nDimension) + 1 Exit Function INVALID_SIZE: ArraySize = -1 End Function Private Function IsUndefined(ByVal vValue As Variant) As Boolean ' Empty / Nothing -> undefined Select Case VBA.VarType(vValue) Case VBA.vbEmpty IsUndefined = True Case VBA.vbObject Select Case VBA.TypeName(vValue) Case "Empty", "Nothing" IsUndefined = True End Select End Select End Function Private Function EncodeStr(ByVal sText As Variant) As String Dim sBuffer$: sBuffer = vbNullString Dim nBufferPosition& Dim nBufferLen& Dim sChar$ Dim nCharCode& Dim nPosition& For nPosition = 1 To VBA.Len(sText) sChar = VBA.Mid$(sText, nPosition, 1) nCharCode = VBA.AscW(sChar) ' Convert negative complement into char code, see: https://support.microsoft.com/en-us/kb/272138 If nCharCode < 0 Then _ nCharCode = nCharCode + 65536 Select Case nCharCode Case 34: sChar = "\""" Case 92: sChar = "\\" Case 47: If escapeSlash_ Then sChar = "\/" Case 8: sChar = "\b" Case 12: sChar = "\f" Case 10: sChar = "\n" Case 13: sChar = "\r" Case 9: sChar = "\t" Case 0 To 31, 127 To 65535: If escapeUnicode_ Then sChar = "\u" & VBA.Right$("0000" & VBA.Hex$(nCharCode), 4) End Select Call BufferAppend(sBuffer, sChar, nBufferPosition, nBufferLen) Next nPosition EncodeStr = BufferToString(sBuffer, nBufferPosition) End Function Private Function IsLargeNumber(sJson As Variant) As Boolean Dim nLength&: nLength = VBA.Len(sJson) If nLength < 16 Or nLength >= 100 Then _ Exit Function IsLargeNumber = True Dim nIndex& For nIndex = 1 To nLength Step 1 Select Case VBA.Asc(VBA.Mid$(sJson, nIndex, 1)) Case 46, 48 To 57, 69, 101 ' Look for .|0-9|E|e ' Continue through characters Case Else IsLargeNumber = False Exit Function End Select Next nIndex End Function Private Function GetWS(nIdent&) As String If Not multiline_ Then _ Exit Function If wsCount_ <> -1 Then GetWS = VBA.Space$(nIdent * wsCount_) Else GetWS = VBA.String$(nIdent, wsCharacter_) End If End Function Private Function BufferAppend(ByRef sBuffer$, ByRef vAppendage As Variant, ByRef nBufferPosition&, ByRef nBufferLen&) ' Allocate a large empty string and then copy string at append position instead of allocating for each append Dim nAppendLen&: nAppendLen = VBA.Len(vAppendage) If nAppendLen + nBufferPosition > nBufferLen Then Dim nAddedLength&: nAddedLength = IIf(nAppendLen > nBufferLen, nAppendLen, nBufferLen) sBuffer = sBuffer & VBA.Space$(nAddedLength) nBufferLen = nBufferLen + nAddedLength End If ' Access and modify buffer using Mid$ Mid$(sBuffer, nBufferPosition + 1, nAppendLen) = CStr(vAppendage) nBufferPosition = nBufferPosition + nAppendLen End Function Private Function BufferToString(ByRef sBuffer$, nBufferPosition&) As String If nBufferPosition > 0 Then _ BufferToString = VBA.Left$(sBuffer, nBufferPosition) End Function