-
Jan 30th, 2018, 04:11 PM
#1
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.
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|