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 '', 66 CHAR(08) 67 UNION ALL 68 SELECT 'f', 69 CHAR(12) 70 UNION ALL 71 SELECT ' ', 72 CHAR(10) 73 UNION ALL 74 SELECT ' ', 75 CHAR(13) 76 UNION ALL 77 SELECT ' ', 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;