private static async ValueTask <JToken> PerformReadJSONTTokenAsync( StreamReaderWithResizableBuffer stream, CharacterReader reader ) { stream.EraseReadBytesFromBuffer(); // Read first non-whitespace character Char charRead; Int32 prevIdx; do { prevIdx = stream.ReadBytesCount; charRead = await reader.ReadNextCharacterAsync(stream); } while (Char.IsWhiteSpace(charRead)); // We know what kind of JToken we will have based on a single character JToken retVal; Boolean encounteredContainerEnd; Int32 startIdx = stream.ReadBytesCount; Int32 curIdx; var encoding = reader.Encoding; switch (charRead) { case ARRAY_END: case OBJ_END: // This happens only when reading empty array/object, and this is called recursively. retVal = null; break; case ARRAY_START: var array = new JArray(); encounteredContainerEnd = false; // Reuse 'retVal' variable since we really need it only at the end of this case block. while (!encounteredContainerEnd && (retVal = await PerformReadJSONTTokenAsync(stream, reader)) != null) { array.Add(retVal); // Read next non-whitespace character - it will be either array value delimiter (',') or array end (']') charRead = await reader.ReadNextCharacterAsync(stream, c => !Char.IsWhiteSpace(c)); encounteredContainerEnd = charRead == ARRAY_END; } retVal = array; break; case OBJ_START: var obj = new JObject(); encounteredContainerEnd = false; String keyStr; // Reuse 'retVal' variable since we really need it only at the end of this case block. while (!encounteredContainerEnd && (keyStr = await ReadJSONStringAsync(reader, stream, false)) != null) { // First JToken should be string being the key // Skip whitespace and ':' charRead = await reader.ReadNextCharacterAsync(stream, c => !Char.IsWhiteSpace( c ) && c != OBJ_KEY_VALUE_DELIM); // Unread previously read character stream.UnreadBytes(reader.GetByteCount(charRead)); // Read another JToken, this one will be our value retVal = await PerformReadJSONTTokenAsync(stream, reader); obj.Add(keyStr, retVal); // Read next non-whitespace character - it will be either object value delimiter (','), or object end ('}') charRead = await reader.ReadNextCharacterAsync(stream, c => !Char.IsWhiteSpace(c)); encounteredContainerEnd = charRead == OBJ_END; } retVal = obj; break; case STR_START: retVal = new JValue(await ReadJSONStringAsync(reader, stream, true)); break; case 't': // Boolean true // read 'r' Validate(await reader.ReadNextCharacterAsync(stream), 'r'); // read 'u' Validate(await reader.ReadNextCharacterAsync(stream), 'u'); // read 'e' Validate(await reader.ReadNextCharacterAsync(stream), 'e'); retVal = new JValue(true); break; case 'f': //Boolean false // read 'a' Validate(await reader.ReadNextCharacterAsync(stream), 'a'); // read 'l' Validate(await reader.ReadNextCharacterAsync(stream), 'l'); // read 's' Validate(await reader.ReadNextCharacterAsync(stream), 's'); // read 'e' Validate(await reader.ReadNextCharacterAsync(stream), 'e'); retVal = new JValue(false); break; case 'n': // null // read 'u' Validate(await reader.ReadNextCharacterAsync(stream), 'u'); // read 'l' Validate(await reader.ReadNextCharacterAsync(stream), 'l'); // read 'l' Validate(await reader.ReadNextCharacterAsync(stream), 'l'); retVal = JValue.CreateNull(); break; default: // The only possibility is number - or malformed JSON string // Read until first non-number-char var lastReadChar = await reader.TryReadNextCharacterAsync(stream, c => { // TODO this isn't strictly according to spec... But will do for now. switch (c) { case '-': // Plus is ok after E/e, Minus is ok at beginning of number, and after E/e case '+': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case (Char)NUMBER_DECIMAL: case (Char)NUMBER_EXP_LOW: case (Char)NUMBER_EXP_UPPER: return(false); default: return(true); } }); // Unread previously read character, unless we arrived at the end. if (lastReadChar.HasValue) { stream.UnreadBytes(reader.GetByteCount(lastReadChar.Value)); } // If we have '.' or 'e' or 'E' then it is non-integer curIdx = prevIdx; var endIdx = stream.ReadBytesCount; Byte curASCIIByte; while ( curIdx < endIdx && (curASCIIByte = encoding.ReadASCIIByte(stream.Buffer, ref curIdx)) != NUMBER_DECIMAL && curASCIIByte != NUMBER_EXP_LOW && curASCIIByte != NUMBER_EXP_UPPER ) { ; } var isInteger = curIdx >= endIdx; var byteCount = endIdx - prevIdx; // TODO maybe use Decimal always for non-integers? PgSQL seems to use NUMERIC for non-integers. retVal = isInteger ? new JValue(encoding.ParseInt64Textual(stream.Buffer, ref prevIdx, (byteCount / encoding.BytesPerASCIICharacter, true))) : new JValue(Double.Parse(encoding.Encoding.GetString(stream.Buffer, prevIdx, byteCount), System.Globalization.CultureInfo.InvariantCulture.NumberFormat)); break; } return(retVal); }