Results 1 to 1 of 1

Thread: Serialize and Deserialize JSON

  1. #1

    Thread Starter
    Super Moderator dday9's Avatar
    Join Date
    Mar 2011
    Location
    South Louisiana
    Posts
    11,760

    Serialize and Deserialize JSON

    This may be my first post in a very long time where I have Option Strict off but I do so because of how I want the short-circuit logical operators to work.

    The code provided uses generics to strictly enforce deserialized values to only include objects that derive from the JSON.ValueBase object.
    Code:
    Namespace JSON
    
        Public Module Convert
    
            ''' <summary>
            ''' Returns a JSON.ValueBase object if the <paramref name="literal"/> is valid JSON
            ''' </summary>
            ''' <param name="literal">The JSON literal to deserialize.</param>
            ''' <returns>JSON.ValueBase</returns>
            Public Function Deserialize(ByVal literal As String) As ValueBase
                'Remove any leading/trailing whitespace
                literal = literal.Trim
    
                'Declare a value to return
                Dim match As JSON.ValueBase = Nothing
    
                'Try to parse a String, Number, Boolean, Null, Array, or Object
                'By using the short-circuit operator AndAlso, if one of the values is a match, then the others won't be bothered to execute
                If Not JSON.TryParseString(literal, 0, match) AndAlso
                            Not JSON.TryParseNumber(literal, 0, match) AndAlso
                            Not JSON.TryParseBoolean(literal, 0, match) AndAlso
                            Not JSON.TryParseNull(literal, 0, match) AndAlso
                            Not JSON.TryParseArray(literal, 0, match) AndAlso
                            Not JSON.TryParseObject(literal, 0, match) Then
                End If
    
                'Return the match (if successful)
                Return match
            End Function
    
            ''' <summary>
            ''' Uses the recursion to check if the <paramref name="source"/> starts with an open bracket, followed by zero or more name/value pairs separated by a comma, followed by a closed bracket
            ''' </summary>
            ''' <param name="source">The JSON literal.</param>
            ''' <param name="index">The index of where the value should appear.</param>
            ''' <param name="conversion">The result of the conversion if a True value is returned from the method.</param>
            ''' <returns>System.Boolean</returns>
            Private Function TryParseObject(ByVal source As String, ByRef index As Integer, ByRef conversion As JSON.Object) As Boolean
                'Declare some temporary values in case the conversion fails
                Dim starting_source As String = source.Substring(index)
                Dim temp_index As Integer = 0
                Dim temp_conversion As JSON.Object = New JSON.Object
    
                'Start by matching an open bracket
                If starting_source(temp_index) = "{"c Then
                    'Increment the index
                    temp_index += 1
    
                    'Skip any whitespace
                    Do While Char.IsWhiteSpace(starting_source(temp_index))
                        temp_index += 1
                    Loop
    
                    'Declare a name and value to match inside the brackets
                    Dim name As JSON.String = Nothing
                    Dim match As JSON.ValueBase = Nothing
    
                    'Loop until we've run out of characters or we've reached a closed bracket
                    Do Until temp_index > starting_source.Length OrElse starting_source(temp_index) = "}"c
                        'Start by getting the name
                        If Not JSON.TryParseString(starting_source, temp_index, name) Then
                            Return False
                        End If
    
                        'Skip any whitespace
                        Do While Char.IsWhiteSpace(starting_source(temp_index))
                            temp_index += 1
                        Loop
    
                        'Make sure that we've reached a name/value separator
                        If starting_source(temp_index) <> ":"c Then
                            Return False
                        End If
    
                        'Increment the index
                        temp_index += 1
    
                        'Skip any whitespace
                        Do While Char.IsWhiteSpace(starting_source(temp_index))
                            temp_index += 1
                        Loop
    
                        'Start by trying to parse a String, Number, Boolean, Null, Array, or Object
                        'By using the short-circuit operator AndAlso, if one of the values is a match, then the others won't be bothered to execute
                        If Not JSON.TryParseString(starting_source, temp_index, match) AndAlso
                            Not JSON.TryParseNumber(starting_source, temp_index, match) AndAlso
                            Not JSON.TryParseBoolean(starting_source, temp_index, match) AndAlso
                            Not JSON.TryParseNull(starting_source, temp_index, match) AndAlso
                            Not JSON.TryParseArray(starting_source, temp_index, match) AndAlso
                            Not JSON.TryParseObject(starting_source, temp_index, match) Then
    
                            Return False
                        End If
    
                        'Skip any whitespace
                        Do While Char.IsWhiteSpace(starting_source(temp_index))
                            temp_index += 1
                        Loop
    
                        'Make sure that we've either reached a separator or closing bracket
                        If starting_source(temp_index) <> ","c AndAlso starting_source(temp_index) <> "}"c Then
                            Return False
                        ElseIf starting_source(temp_index) = ","c Then
                            'If we're at a separator, increment the index
                            temp_index += 1
                        End If
    
                        'Add the name/value to the collection
                        temp_conversion.Values.Add(name.Value, match)
                        match = Nothing 'Reset the variable
    
                        'Skip any whitespace
                        Do While Char.IsWhiteSpace(starting_source(temp_index))
                            temp_index += 1
                        Loop
                    Loop
    
                    'Skip any whitespace
                    Do While Char.IsWhiteSpace(starting_source(temp_index))
                        temp_index += 1
                    Loop
    
                    'Ensure we've hit a closed bracket
                    If starting_source(temp_index) = "}"c Then
                        'Increment the index and set the ByRef parameter
                        index += temp_index + 1
                        conversion = temp_conversion
    
                        Return True
                    End If
    
                End If
    
                Return False
            End Function
    
            ''' <summary>
            ''' Uses the recursion to check if the <paramref name="source"/> starts with an open bracket, followed by zero or more values separated by a comma, followed by a closed bracket
            ''' </summary>
            ''' <param name="source">The JSON literal.</param>
            ''' <param name="index">The index of where the value should appear.</param>
            ''' <param name="conversion">The result of the conversion if a True value is returned from the method.</param>
            ''' <returns>System.Boolean</returns>
            Private Function TryParseArray(ByVal source As String, ByRef index As Integer, ByRef conversion As JSON.Array) As Boolean
                'Declare some temporary values in case the conversion fails
                Dim starting_source As String = source.Substring(index)
                Dim temp_index As Integer = 0
                Dim temp_conversion As JSON.Array = New JSON.Array
    
                'Start by matching an open bracket
                If starting_source(temp_index) = "["c Then
                    'Increment the index
                    temp_index += 1
    
                    'Skip any whitespace
                    Do While Char.IsWhiteSpace(starting_source(temp_index))
                        temp_index += 1
                    Loop
    
                    'Declare a value to match inside the brackets
                    Dim match As JSON.ValueBase = Nothing
    
                    'Loop until we've run out of characters or we've reached a closed bracket
                    Do Until temp_index > starting_source.Length OrElse starting_source(temp_index) = "]"c
                        'Start by trying to parse a String, Number, Boolean, Null, Array, or Object
                        'By using the short-circuit operator AndAlso, if one of the values is a match, then the others won't be bothered to execute
                        If Not JSON.TryParseString(starting_source, temp_index, match) AndAlso
                            Not JSON.TryParseNumber(starting_source, temp_index, match) AndAlso
                            Not JSON.TryParseBoolean(starting_source, temp_index, match) AndAlso
                            Not JSON.TryParseNull(starting_source, temp_index, match) AndAlso
                            Not JSON.TryParseArray(starting_source, temp_index, match) AndAlso
                            Not JSON.TryParseObject(starting_source, temp_index, match) Then
    
                            Return False
                        End If
    
                        'Skip any whitespace
                        Do While Char.IsWhiteSpace(starting_source(temp_index))
                            temp_index += 1
                        Loop
    
                        'Make sure that we've either reached a separator or closing bracket
                        If starting_source(temp_index) <> ","c AndAlso starting_source(temp_index) <> "]"c Then
                            Return False
                        ElseIf starting_source(temp_index) = ","c Then
                            'If we're at a separator, increment the index
                            temp_index += 1
                        End If
    
                        'Add the value to the collection
                        temp_conversion.Values.Add(match)
                        match = Nothing 'Reset the variable
    
                        'Skip any whitespace
                        Do While Char.IsWhiteSpace(starting_source(temp_index))
                            temp_index += 1
                        Loop
                    Loop
    
                    'Skip any whitespace
                    Do While Char.IsWhiteSpace(starting_source(temp_index))
                        temp_index += 1
                    Loop
    
                    'Ensure we've hit a closed bracket
                    If starting_source(temp_index) = "]"c Then
                        'Increment the index and set the ByRef parameter
                        index += temp_index + 1
                        conversion = temp_conversion
    
                        Return True
                    End If
                End If
    
                Return False
            End Function
    
            ''' <summary>
            ''' Uses the RegEx to check if the <paramref name="source"/> starts with a literal that following this pattern: <c>""|"([^\\"\x00-\x1F\x7F]|(\\\"|\\\\|\\n|\\b|\\f|\\t|\\r|\\u[0-9a-fA-F]{4}))*"</c>.
            ''' </summary>
            ''' <param name="source">The JSON literal.</param>
            ''' <param name="index">The index of where the value should appear.</param>
            ''' <param name="conversion">The result of the conversion if a True value is returned from the method.</param>
            ''' <remarks>The RegEx pattern is a little complicated to explain by breaking down each individual component. Instead, what it does is:
            ''' Try to match an empty String
            ''' Match any character that is not an escape character or ASCII character 0-31 and 127 zero or more times
            ''' If a reverse solidus is found, it ensures that there is an escaping character that follows</remarks>
            ''' <returns>System.Boolean</returns>
            Private Function TryParseString(ByVal source As String, ByRef index As Integer, ByRef conversion As JSON.String) As Boolean
                Dim double_quote As Char = """"c
                Dim match_four As String = "{4}"
                Dim r As Text.RegularExpressions.Regex = New Text.RegularExpressions.Regex($"({double_quote}{double_quote}|{double_quote}([^\\{double_quote}\x00-\x1F\x7F]|(\\\{double_quote}|\\\\|\\n|\\b|\\f|\\t|\\r|\\u[0-9a-fA-F]{match_four}))*{double_quote})")
                Dim m As Text.RegularExpressions.Match = r.Match(source, index)
                Dim success As Boolean = m.Success AndAlso m.Index = index
    
                If success Then
                    conversion = New JSON.String() With {.Value = m.Value.Substring(1, m.Value.Length - 2)}
                    index += m.Value.Length
                End If
    
                Return success
            End Function
    
            ''' <summary>
            ''' Uses the RegEx to check if the <paramref name="source"/> starts with a literal that following this pattern: <c>[+-]?\d+(\.\d+)?([eE][+-]?\d+)?</c>.
            ''' </summary>
            ''' <param name="source">The JSON literal.</param>
            ''' <param name="index">The index of where the value should appear.</param>
            ''' <param name="conversion">The result of the conversion if a True value is returned from the method.</param>
            ''' <remarks>The RegEx defined:
            ''' <c>[+-]?</c> Optionally matches a unary operator
            ''' <c>\d+</c> Matches one or more digits
            ''' <c>(\.\d+)?</c> Optionally matches a decimal followed by one or more digits
            ''' <c>([eE][+-]?\d+)?</c> Optionally matches a euler operator followed by an optional unary operator followed by one or more digits
            ''' </remarks>
            ''' <returns>System.Boolean</returns>
            Private Function TryParseNumber(ByVal source As String, ByRef index As Integer, ByRef conversion As JSON.Number) As Boolean
                Dim r As Text.RegularExpressions.Regex = New Text.RegularExpressions.Regex("[+-]?\d+(\.\d+)?([eE][+-]?\d+)?")
                Dim m As Text.RegularExpressions.Match = r.Match(source, index)
                Dim success As Boolean = m.Success AndAlso m.Index = index
    
                If success Then
                    conversion = New JSON.Number() With {.Value = Double.Parse(m.Value)}
                    index += m.Value.Length
                End If
    
                Return success
            End Function
    
            ''' <summary>
            ''' Uses the StartsWith method to check if the <paramref name="source"/> starts with the literal values <c>true or false</c> at the given <paramref name="index"/>.
            ''' </summary>
            ''' <param name="source">The JSON literal.</param>
            ''' <param name="index">The index of where the value should appear.</param>
            ''' <param name="conversion">The result of the conversion if a True value is returned from the method.</param>
            ''' <returns>System.Boolean</returns>
            Private Function TryParseBoolean(ByVal source As String, ByRef index As Integer, ByRef conversion As JSON.Boolean) As Boolean
                Dim starting_source As String = source.Substring(index)
                Dim success As Boolean =
                    starting_source.StartsWith("true", StringComparison.OrdinalIgnoreCase) OrElse
                    starting_source.StartsWith("false", StringComparison.OrdinalIgnoreCase)
    
                If success Then
                    conversion = New JSON.Boolean() With {.Value = starting_source.StartsWith("true", StringComparison.OrdinalIgnoreCase)}
    
                    index += 4
                    If Not conversion.Value Then
                        index += 1
                    End If
                End If
    
                Return success
            End Function
    
            ''' <summary>
            ''' Uses the StartsWith method to check if the <paramref name="source"/> starts with the literal value <c>null</c> at the given <paramref name="index"/>.
            ''' </summary>
            ''' <param name="source">The JSON literal.</param>
            ''' <param name="index">The index of where the value should appear.</param>
            ''' <param name="conversion">The result of the conversion if a True value is returned from the method.</param>
            ''' <returns>System.Boolean</returns>
            Private Function TryParseNull(ByVal source As String, ByRef index As Integer, ByRef conversion As JSON.ValueBase) As Boolean
                Dim success As Boolean = source.Substring(index).StartsWith("null", StringComparison.OrdinalIgnoreCase)
    
                If success Then
                    conversion = New JSON.ValueBase
                End If
    
                Return success
            End Function
    
        End Module
    
        Public MustInherit Class Value(Of T As ValueBase)
    
            Public MustOverride ReadOnly Property Value As T
    
        End Class
    
        Public Class ValueBase
    
            Public Overrides Function ToString() As String
                Return "null"
            End Function
    
        End Class
    
        Public Class [Boolean]
            Inherits JSON.ValueBase
    
            Public Property Value As Boolean
    
            Public Overrides Function ToString() As String
                Return Value.ToString.ToLower()
            End Function
    
        End Class
    
        Public Class Number
            Inherits JSON.ValueBase
    
            Public Property Value As Double
    
            Public Overrides Function ToString() As String
                Return Value.ToString
            End Function
    
        End Class
    
        Public Class [String]
            Inherits JSON.ValueBase
    
            Public Property Value As String
    
            Public Overrides Function ToString() As String
                Return """" & Value & """"
            End Function
    
        End Class
    
        Public Class [Object]
            Inherits JSON.ValueBase
    
            Public Property Values As Dictionary(Of String, JSON.ValueBase)
    
            Sub New()
                Me.Values = New Dictionary(Of String, JSON.ValueBase)
            End Sub
    
            Public Overrides Function ToString() As String
                Return "{" & String.Join(",", Me.Values.Select(Function(kvp) """" & kvp.Key & """" & ":" & kvp.Value.ToString()).ToArray()) & "}"
            End Function
    
        End Class
    
        Public Class [Array]
            Inherits JSON.ValueBase
    
            Public ReadOnly Property Values As List(Of ValueBase)
    
            Sub New()
                Me.Values = New List(Of ValueBase)
            End Sub
    
            Public Overrides Function ToString() As String
                Return $"[{String.Join(",", Me.Values)}]"
            End Function
    
        End Class
    
    End Namespace
    Last edited by dday9; Jan 30th, 2018 at 04:26 PM.
    "Code is like humor. When you have to explain it, it is bad." - Cory House
    VbLessons | Code Tags | Sword of Fury - Jameram

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width