1 CREATE FUNCTION [dbo].[parseJSON] (@Json NVARCHAR(MAX)) 2 RETURNS @hierarchy TABLE (element_id INT IDENTITY(1, 1) NOT NULL, /* internal surrogate primary key gives the order of parsing and the list order */ 3 sequenceNo [INT] NULL, /* the place in the sequence for the element */ 4 parent_ID INT, /* if the element has a parent then it is in this column. The document is the ultimate parent, so you can get the structure from recursing from the document */ 5 Object_ID INT, /* each list or object has an object id. This ties all elements to a parent. Lists are treated as objects here */ 6 NAME NVARCHAR(2000), /* the name of the object */ 7 StringValue NVARCHAR(MAX) NOT NULL, /*the string representation of the value of the element. */ 8 ValueType VARCHAR(10) NOT NULL /* the declared type of the value represented as a string in StringValue*/ 9 ) 10 AS 11 BEGIN 12 13 DECLARE @FirstObject INT, --the index of the first open bracket found in the JSON string 14 @OpenDelimiter INT, --the index of the next open bracket found in the JSON string 15 @NextOpenDelimiter INT, --the index of subsequent open bracket found in the JSON string 16 @NextCloseDelimiter INT, --the index of subsequent close bracket found in the JSON string 17 @Type NVARCHAR(10), --whether it denotes an object or an array 18 @NextCloseDelimiterChar CHAR(1), --either a '}' or a ']' 19 @Contents NVARCHAR(MAX), --the unparsed contents of the bracketed expression 20 @Start INT, --index of the start of the token that you are parsing 21 @end INT, --index of the end of the token that you are parsing 22 @param INT, --the parameter at the end of the next Object/Array token 23 @EndOfName INT, --the index of the start of the parameter at end of Object/Array token 24 @token NVARCHAR(200), --either a string or object 25 @value NVARCHAR(MAX), -- the value as a string 26 @SequenceNo INT, -- the sequence number within a list 27 @name NVARCHAR(200), --the name as a string 28 @parent_ID INT, --the next parent ID to allocate 29 @lenJSON INT, --the current length of the JSON String 30 @characters NCHAR(36), --used to convert hex to decimal 31 @result BIGINT, --the value of the hex symbol being parsed 32 @index SMALLINT, --used for parsing the hex value 33 @Escape INT; --the index of the next escape character 34 DECLARE @Strings TABLE /* in this temporary table we keep all strings, even the names of the elements, since they are 'escaped' in a different way, and may contain, unescaped, brackets denoting objects or lists. These are replaced in the JSON string b 35 y tokens representing the string */ 36 (String_ID INT IDENTITY(1, 1), 37 StringValue NVARCHAR(MAX)); 38 SELECT --initialise the characters to convert hex to ascii 39 @characters = N'0123456789abcdefghijklmnopqrstuvwxyz', 40 @SequenceNo = 0, --set the sequence no. to something sensible. 41 @parent_ID = 0; 42 WHILE 1 = 1 --forever until there is nothing more to do 43 BEGIN 44 SELECT @Start = PATINDEX('%[^a-zA-Z]["]%', @Json COLLATE SQL_Latin1_General_CP850_BIN); --next delimited string 45 IF @Start = 0 46 BREAK; --no more so drop through the WHILE loop 47 IF SUBSTRING(@Json, @Start + 1, 1) = '"' 48 BEGIN --Delimited Name 49 SET @Start = @Start + 1; 50 SET @end = PATINDEX('%[^\]["]%', RIGHT(@Json, LEN(@Json + '|') - @Start)); 51 END; 52 IF @end = 0 --no end delimiter to last string 53 BREAK; --no more 54 SELECT @token = SUBSTRING(@Json, @Start + 1, @end - 1); 55 SELECT @token = REPLACE(@token, substitutions.FromString, substitutions.ToString) 56 FROM ( SELECT '\"' AS FromString, 57 '"' AS ToString 58 UNION ALL 59 SELECT '\\', 60 '\' 61 UNION ALL 62 SELECT '\/', 63 '/' 64 UNION ALL 65 SELECT '\b', 66 CHAR(08) 67 UNION ALL 68 SELECT '\f', 69 CHAR(12) 70 UNION ALL 71 SELECT '\n', 72 CHAR(10) 73 UNION ALL 74 SELECT '\r', 75 CHAR(13) 76 UNION ALL 77 SELECT '\t', 78 CHAR(09) 79 UNION ALL 80 SELECT '゛', 81 CHAR(09)) substitutions; 82 SELECT @result = 0, 83 @Escape = 1; 84 WHILE @Escape > 0 85 BEGIN 86 SELECT @index = 0, 87 @Escape = PATINDEX('%\x[0-9a-f][0-9a-f][0-9a-f][0-9a-f]%', @token); 88 IF @Escape > 0 --if there is one 89 BEGIN 90 WHILE @index < 4 --there are always four digits to a \x sequence 91 BEGIN 92 SELECT --determine its value 93 @result 94 = @result + POWER(16, @index) 95 * (CHARINDEX(SUBSTRING(@token, @Escape + 2 + 3 - @index, 1), @characters) - 1), 96 @index = @index + 1; 97 END; 98 SELECT @token = STUFF(@token, @Escape, 6, NCHAR(@result)); 99 END; 100 END; 101 INSERT INTO @Strings (StringValue) 102 SELECT @token; 103 SELECT @Json = STUFF(@Json, @Start, @end + 1, '@string' + CONVERT(NVARCHAR(5), @@identity)); 104 END; 105 WHILE 1 = 1 --forever until there is nothing more to do 106 BEGIN 107 SELECT @parent_ID = @parent_ID + 1; 108 SELECT @FirstObject = PATINDEX('%[{[[]%', @Json COLLATE SQL_Latin1_General_CP850_BIN); --object or array 109 IF @FirstObject = 0 110 BREAK; 111 IF (SUBSTRING(@Json, @FirstObject, 1) = '{') 112 SELECT @NextCloseDelimiterChar = '}', 113 @Type = 'object'; 114 ELSE 115 SELECT @NextCloseDelimiterChar = ']', 116 @Type = 'array'; 117 SELECT @OpenDelimiter = @FirstObject; 118 WHILE 1 = 1 --find the innermost object or list... 119 BEGIN 120 SELECT @lenJSON = LEN(@Json + '|') - 1; 121 SELECT @NextCloseDelimiter = CHARINDEX(@NextCloseDelimiterChar, @Json, @OpenDelimiter + 1); 122 SELECT @NextOpenDelimiter 123 = PATINDEX('%[{[[]%', RIGHT(@Json, @lenJSON - @OpenDelimiter)COLLATE SQL_Latin1_General_CP850_BIN); --object 124 IF @NextOpenDelimiter = 0 125 BREAK; 126 SELECT @NextOpenDelimiter = @NextOpenDelimiter + @OpenDelimiter; 127 IF @NextCloseDelimiter < @NextOpenDelimiter 128 BREAK; 129 IF SUBSTRING(@Json, @NextOpenDelimiter, 1) = '{' 130 SELECT @NextCloseDelimiterChar = '}', 131 @Type = 'object'; 132 ELSE 133 SELECT @NextCloseDelimiterChar = ']', 134 @Type = 'array'; 135 SELECT @OpenDelimiter = @NextOpenDelimiter; 136 END; 137 SELECT @Contents = SUBSTRING(@Json, @OpenDelimiter + 1, @NextCloseDelimiter - @OpenDelimiter - 1); 138 SELECT @Json 139 = STUFF( 140 @Json, 141 @OpenDelimiter, 142 @NextCloseDelimiter - @OpenDelimiter + 1, 143 '@' + @Type + CONVERT(NVARCHAR(5), @parent_ID)); 144 WHILE (PATINDEX('%[A-Za-z0-9@+.e]%', @Contents COLLATE SQL_Latin1_General_CP850_BIN)) <> 0 145 BEGIN 146 IF @Type = 'Object' --it will be a 0-n list containing a string followed by a string, number,boolean, or null 147 BEGIN 148 SELECT @SequenceNo = 0, 149 @end = CHARINDEX(':', ' ' + @Contents); --if there is anything, it will be a string-based name. 150 SELECT @Start = PATINDEX('%[^A-Za-z@][@]%', ' ' + @Contents); --AAAAAAAA 151 SELECT @token = SUBSTRING(' ' + @Contents, @Start + 1, @end - @Start - 1), 152 @EndOfName = PATINDEX('%[0-9]%', @token COLLATE SQL_Latin1_General_CP850_BIN), 153 @param = RIGHT(@token, LEN(@token) - @EndOfName + 1); 154 SELECT @token = LEFT(@token, @EndOfName - 1), 155 @Contents = RIGHT(' ' + @Contents, LEN(' ' + @Contents + '|') - @end - 1); 156 SELECT @name = StringValue 157 FROM @Strings 158 WHERE String_ID = @param; --fetch the name 159 END; 160 ELSE 161 SELECT @name = NULL, 162 @SequenceNo = @SequenceNo + 1; 163 SELECT @end = CHARINDEX(',', @Contents); -- a string-token, object-token, list-token, number,boolean, or null 164 IF @end = 0 165 SELECT @end = PATINDEX('%[A-Za-z0-9@+.e][^A-Za-z0-9@+.e]%', @Contents + ' ') + 1; 166 SELECT @Start = PATINDEX('%[^A-Za-z0-9@+.e][A-Za-z0-9@+.e]%', ' ' + @Contents); 167 --select @start,@end, LEN(@contents+'|'), @contents 168 SELECT @value = RTRIM(SUBSTRING(@Contents, @Start, @end - @Start)), 169 @Contents = RIGHT(@Contents + ' ', LEN(@Contents + '|') - @end); 170 IF SUBSTRING(@value, 1, 7) = '@object' 171 INSERT INTO @hierarchy (NAME, 172 sequenceNo, 173 parent_ID, 174 StringValue, 175 Object_ID, 176 ValueType) 177 SELECT @name, 178 @SequenceNo, 179 @parent_ID, 180 SUBSTRING(@value, 8, 5), 181 SUBSTRING(@value, 8, 5), 182 'object'; 183 ELSE IF SUBSTRING(@value, 1, 6) = '@array' 184 INSERT INTO @hierarchy (NAME, 185 sequenceNo, 186 parent_ID, 187 StringValue, 188 Object_ID, 189 ValueType) 190 SELECT @name, 191 @SequenceNo, 192 @parent_ID, 193 SUBSTRING(@value, 7, 5), 194 SUBSTRING(@value, 7, 5), 195 'array'; 196 ELSE IF SUBSTRING(@value, 1, 7) = '@string' 197 INSERT INTO @hierarchy (NAME, 198 sequenceNo, 199 parent_ID, 200 StringValue, 201 ValueType) 202 SELECT @name, 203 @SequenceNo, 204 @parent_ID, 205 StringValue, 206 'string' 207 FROM @Strings 208 WHERE String_ID = SUBSTRING(@value, 8, 5); 209 ELSE IF @value IN ( 'true', 'false' ) 210 INSERT INTO @hierarchy (NAME, 211 sequenceNo, 212 parent_ID, 213 StringValue, 214 ValueType) 215 SELECT @name, 216 @SequenceNo, 217 @parent_ID, 218 @value, 219 'boolean'; 220 ELSE IF @value = 'null' 221 INSERT INTO @hierarchy (NAME, 222 sequenceNo, 223 parent_ID, 224 StringValue, 225 ValueType) 226 SELECT @name, 227 @SequenceNo, 228 @parent_ID, 229 @value, 230 'null'; 231 ELSE IF PATINDEX('%[^0-9]%', @value COLLATE SQL_Latin1_General_CP850_BIN) > 0 232 INSERT INTO @hierarchy (NAME, 233 sequenceNo, 234 parent_ID, 235 StringValue, 236 ValueType) 237 SELECT @name, 238 @SequenceNo, 239 @parent_ID, 240 @value, 241 'real'; 242 ELSE 243 INSERT INTO @hierarchy (NAME, 244 sequenceNo, 245 parent_ID, 246 StringValue, 247 ValueType) 248 SELECT @name, 249 @SequenceNo, 250 @parent_ID, 251 @value, 252 'int'; 253 254 IF @Contents = ' ' 255 SELECT @SequenceNo = 0; 256 END; 257 END; 258 RETURN; 259 END;