Introduce JsonDocument
authorJeremy Barton <jbarton@microsoft.com>
Wed, 23 Jan 2019 00:56:47 +0000 (16:56 -0800)
committerGitHub <noreply@github.com>
Wed, 23 Jan 2019 00:56:47 +0000 (16:56 -0800)
JsonDocument is a read-only random-access document model for JSON payloads.

The JsonDocument class holds parsing state, and carves off JsonElement
and JsonProperty cursors as needed to represent different locations within
the logical document.

Commit migrated from https://github.com/dotnet/corefx/commit/2d9dd533e16889ea4d279162df4039982bfbbca2

27 files changed:
src/libraries/System.Text.Json/ref/System.Text.Json.cs
src/libraries/System.Text.Json/src/Resources/Strings.resx
src/libraries/System.Text.Json/src/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs
src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.DbRow.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.MetadataDb.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.Parse.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.StackRow.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.StackRowStack.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.TryGetProperty.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/JsonElement.ArrayEnumerator.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/JsonElement.ObjectEnumerator.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/JsonElement.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/JsonProperty.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/JsonValueType.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs
src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs
src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderState.cs
src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.MultiSegment.cs
src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs
src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs
src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs
src/libraries/System.Text.Json/tests/JsonDocumentTests.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/JsonTestHelper.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj
src/libraries/pkg/descriptions.json

index f3f6cc8..85417ed 100644 (file)
@@ -13,6 +13,86 @@ namespace System.Text.Json
         Disallow = (byte)0,
         Skip = (byte)2,
     }
+    public sealed partial class JsonDocument : System.IDisposable
+    {
+        internal JsonDocument() { }
+        public System.Text.Json.JsonElement RootElement { get { throw null; } }
+        public void Dispose() { }
+        public static System.Text.Json.JsonDocument Parse(System.Buffers.ReadOnlySequence<byte> utf8Json, System.Text.Json.JsonReaderOptions readerOptions = default(System.Text.Json.JsonReaderOptions)) { throw null; }
+        public static System.Text.Json.JsonDocument Parse(System.IO.Stream utf8Json, System.Text.Json.JsonReaderOptions readerOptions = default(System.Text.Json.JsonReaderOptions)) { throw null; }
+        public static System.Text.Json.JsonDocument Parse(System.ReadOnlyMemory<byte> utf8Json, System.Text.Json.JsonReaderOptions readerOptions = default(System.Text.Json.JsonReaderOptions)) { throw null; }
+        public static System.Text.Json.JsonDocument Parse(System.ReadOnlyMemory<char> json, System.Text.Json.JsonReaderOptions readerOptions = default(System.Text.Json.JsonReaderOptions)) { throw null; }
+        public static System.Text.Json.JsonDocument Parse(string json, System.Text.Json.JsonReaderOptions readerOptions = default(System.Text.Json.JsonReaderOptions)) { throw null; }
+        public static System.Threading.Tasks.Task<System.Text.Json.JsonDocument> ParseAsync(System.IO.Stream utf8Json, System.Text.Json.JsonReaderOptions readerOptions = default(System.Text.Json.JsonReaderOptions), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
+    }
+    public readonly partial struct JsonElement
+    {
+        private readonly object _dummy;
+        public System.Text.Json.JsonElement this[int index] { get { throw null; } }
+        public System.Text.Json.JsonValueType Type { get { throw null; } }
+        public System.Text.Json.JsonElement.ArrayEnumerator EnumerateArray() { throw null; }
+        public System.Text.Json.JsonElement.ObjectEnumerator EnumerateObject() { throw null; }
+        public int GetArrayLength() { throw null; }
+        public bool GetBoolean() { throw null; }
+        public decimal GetDecimal() { throw null; }
+        public double GetDouble() { throw null; }
+        public int GetInt32() { throw null; }
+        public long GetInt64() { throw null; }
+        public System.Text.Json.JsonElement GetProperty(System.ReadOnlySpan<byte> utf8PropertyName) { throw null; }
+        public System.Text.Json.JsonElement GetProperty(System.ReadOnlySpan<char> propertyName) { throw null; }
+        public System.Text.Json.JsonElement GetProperty(string propertyName) { throw null; }
+        public string GetRawText() { throw null; }
+        public float GetSingle() { throw null; }
+        public string GetString() { throw null; }
+        [System.CLSCompliantAttribute(false)]
+        public uint GetUInt32() { throw null; }
+        [System.CLSCompliantAttribute(false)]
+        public ulong GetUInt64() { throw null; }
+        public override string ToString() { throw null; }
+        public bool TryGetDecimal(out decimal value) { throw null; }
+        public bool TryGetDouble(out double value) { throw null; }
+        public bool TryGetInt32(out int value) { throw null; }
+        public bool TryGetInt64(out long value) { throw null; }
+        public bool TryGetProperty(System.ReadOnlySpan<byte> utf8PropertyName, out System.Text.Json.JsonElement value) { throw null; }
+        public bool TryGetProperty(System.ReadOnlySpan<char> propertyName, out System.Text.Json.JsonElement value) { throw null; }
+        public bool TryGetProperty(string propertyName, out System.Text.Json.JsonElement value) { throw null; }
+        public bool TryGetSingle(out float value) { throw null; }
+        [System.CLSCompliantAttribute(false)]
+        public bool TryGetUInt32(out uint value) { throw null; }
+        [System.CLSCompliantAttribute(false)]
+        public bool TryGetUInt64(out ulong value) { throw null; }
+        public partial struct ArrayEnumerator : System.Collections.Generic.IEnumerable<System.Text.Json.JsonElement>, System.Collections.Generic.IEnumerator<System.Text.Json.JsonElement>, System.Collections.IEnumerable, System.Collections.IEnumerator, System.IDisposable
+        {
+            private object _dummy;
+            public System.Text.Json.JsonElement Current { get { throw null; } }
+            object System.Collections.IEnumerator.Current { get { throw null; } }
+            public void Dispose() { }
+            public System.Text.Json.JsonElement.ArrayEnumerator GetEnumerator() { throw null; }
+            public bool MoveNext() { throw null; }
+            public void Reset() { }
+            System.Collections.Generic.IEnumerator<System.Text.Json.JsonElement> System.Collections.Generic.IEnumerable<System.Text.Json.JsonElement>.GetEnumerator() { throw null; }
+            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
+        }
+        public partial struct ObjectEnumerator : System.Collections.Generic.IEnumerable<System.Text.Json.JsonProperty>, System.Collections.Generic.IEnumerator<System.Text.Json.JsonProperty>, System.Collections.IEnumerable, System.Collections.IEnumerator, System.IDisposable
+        {
+            private object _dummy;
+            public System.Text.Json.JsonProperty Current { get { throw null; } }
+            object System.Collections.IEnumerator.Current { get { throw null; } }
+            public void Dispose() { }
+            public System.Text.Json.JsonElement.ObjectEnumerator GetEnumerator() { throw null; }
+            public bool MoveNext() { throw null; }
+            public void Reset() { }
+            System.Collections.Generic.IEnumerator<System.Text.Json.JsonProperty> System.Collections.Generic.IEnumerable<System.Text.Json.JsonProperty>.GetEnumerator() { throw null; }
+            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
+        }
+    }
+    public readonly partial struct JsonProperty
+    {
+        private readonly object _dummy;
+        public string Name { get { throw null; } }
+        public System.Text.Json.JsonElement Value { get { throw null; } }
+        public override string ToString() { throw null; }
+    }
     public sealed partial class JsonReaderException : System.Exception
     {
         public JsonReaderException(string message, long lineNumber, long bytePositionInLine) { }
@@ -49,6 +129,17 @@ namespace System.Text.Json
         String = (byte)6,
         True = (byte)8,
     }
+    public enum JsonValueType : byte
+    {
+        Array = (byte)2,
+        False = (byte)6,
+        Null = (byte)7,
+        Number = (byte)4,
+        Object = (byte)1,
+        String = (byte)3,
+        True = (byte)5,
+        Undefined = (byte)0,
+    }
     public partial struct JsonWriterOptions
     {
         private object _dummy;
index b498f4f..ddab22e 100644 (file)
   <data name="InvalidHexCharacterWithinString" xml:space="preserve">
     <value>'{0}' is not a hex digit following '\u' within a JSON string. The string should be correctly escaped.</value>
   </data>
+  <data name="JsonDocumentDoesNotSupportComments" xml:space="preserve">
+    <value>Comments cannot be stored in a JsonDocument, only the Skip and Disallow comment handling modes are supported.</value>
+  </data>
+  <data name="JsonElementHasWrongType" xml:space="preserve">
+    <value>The requested operation requires an element of type '{0}', but the target element has type '{1}'.</value>
+  </data>
   <data name="MaxDepthMustBePositive" xml:space="preserve">
     <value>Max depth must be positive.</value>
   </data>
   <data name="FormatSingle" xml:space="preserve">
     <value>The JSON value is either too large or too small for a Single.</value>
   </data>
+  <data name="FormatUInt32" xml:space="preserve">
+    <value>The JSON value is either of incorrect numeric format, or too large or too small for a UInt32.</value>
+  </data>
+  <data name="FormatUInt64" xml:space="preserve">
+    <value>The JSON value is either of incorrect numeric format, or too large or too small for a UInt64.</value>
+  </data>
   <data name="RequiredDigitNotFoundAfterDecimal" xml:space="preserve">
     <value>'{0}' is invalid within a number, immediately after a decimal point ('.'). Expected a digit ('0'-'9').</value>
   </data>
   <data name="ZeroDepthAtEnd" xml:space="preserve">
     <value>Expected CurrentDepth ({0}) to be zero at the end of the JSON payload. There is an open JSON object or array that should be closed.</value>
   </data>
-  <data name="FormatUInt32" xml:space="preserve">
-    <value>The JSON value is either of incorrect numeric format, or too large or too small for a UInt32.</value>
-  </data>
-  <data name="FormatUInt64" xml:space="preserve">
-    <value>The JSON value is either of incorrect numeric format, or too large or too small for a UInt64.</value>
-  </data>
 </root>
index 6d9dd46..39cba2b 100644 (file)
     <Compile Include="System\Text\Json\BitStack.cs" />
     <Compile Include="System\Text\Json\JsonCommentHandling.cs" />
     <Compile Include="System\Text\Json\JsonConstants.cs" />
+    <Compile Include="System\Text\Json\JsonDocument.cs" />
+    <Compile Include="System\Text\Json\JsonDocument.DbRow.cs" />
+    <Compile Include="System\Text\Json\JsonDocument.MetadataDb.cs" />
+    <Compile Include="System\Text\Json\JsonDocument.Parse.cs" />
+    <Compile Include="System\Text\Json\JsonDocument.StackRow.cs" />
+    <Compile Include="System\Text\Json\JsonDocument.StackRowStack.cs" />
+    <Compile Include="System\Text\Json\JsonDocument.TryGetProperty.cs" />
+    <Compile Include="System\Text\Json\JsonElement.cs" />
+    <Compile Include="System\Text\Json\JsonElement.ArrayEnumerator.cs" />
+    <Compile Include="System\Text\Json\JsonElement.ObjectEnumerator.cs" />
+    <Compile Include="System\Text\Json\JsonProperty.cs" />
     <Compile Include="System\Text\Json\JsonHelpers.cs" />
     <Compile Include="System\Text\Json\JsonTokenType.cs" />
+    <Compile Include="System\Text\Json\JsonValueType.cs" />
     <Compile Include="System\Text\Json\ThrowHelper.cs" />
     <Compile Include="System\Text\Json\Reader\ConsumeNumberResult.cs" />
     <Compile Include="System\Text\Json\Reader\ConsumeTokenResult.cs" />
         <_sourceToPackage Include="$(_generatedSourcePackageTargetPath)" PackagePath="build/netstandard2.0/$(_targetsFileName)" />
       </ItemGroup>
   </Target>
-</Project>
\ No newline at end of file
+</Project>
index 2b6960c..2fc3b30 100644 (file)
@@ -57,6 +57,8 @@ namespace System.Text.Json
         public const int MaximumFormatDateTimeLength = 27;    // StandardFormat 'O', e.g. 2017-06-12T05:30:45.7680000
         public const int MaximumFormatDateTimeOffsetLength = 33;  // StandardFormat 'O', e.g. 2017-06-12T05:30:45.7680000-07:00
 
+        internal const char ScientificNotationFormat = 'e';
+
         // Encoding Helpers
         public const char HighSurrogateStart = '\ud800';
         public const char HighSurrogateEnd = '\udbff';
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.DbRow.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.DbRow.cs
new file mode 100644 (file)
index 0000000..78fb28d
--- /dev/null
@@ -0,0 +1,78 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace System.Text.Json
+{
+    public sealed partial class JsonDocument
+    {
+        [StructLayout(LayoutKind.Sequential)]
+        internal struct DbRow
+        {
+            internal const int Size = 12;
+
+            // Sign bit is currently unassigned
+            private int _location;
+
+            // Sign bit is used for "HasComplexChildren" (StartArray)
+            private int _sizeOrLengthUnion;
+
+            // Top nybble is JsonTokenType
+            // remaining nybbles are the number of rows to skip to get to the next value
+            // This isn't limiting on the number of rows, since Span.MaxLength / sizeof(DbRow) can't
+            // exceed that range.
+            private readonly int _numberOfRowsAndTypeUnion;
+
+            /// <summary>
+            /// Index into the payload
+            /// </summary>
+            internal int Location => _location;
+
+            /// <summary>
+            /// length of text in JSON payload (or number of elements if its a JSON array)
+            /// </summary>
+            internal int SizeOrLength => _sizeOrLengthUnion & int.MaxValue;
+
+            internal bool IsUnknownSize => _sizeOrLengthUnion == UnknownSize;
+
+            /// <summary>
+            /// Number: Use scientific format.
+            /// String/PropertyName: Unescaping is required.
+            /// Array: At least one element is an object/array.
+            /// Otherwise; false
+            /// </summary>
+            internal bool HasComplexChildren => _sizeOrLengthUnion < 0;
+
+            internal int NumberOfRows =>
+                _numberOfRowsAndTypeUnion & 0x0FFFFFFF; // Number of rows that the current JSON element occupies within the database
+
+            internal JsonTokenType TokenType => (JsonTokenType)(unchecked((uint)_numberOfRowsAndTypeUnion) >> 28);
+
+            internal const int UnknownSize = -1;
+
+#if DEBUG
+            static unsafe DbRow()
+            {
+                Debug.Assert(sizeof(DbRow) == Size);
+            }
+#endif
+
+            internal DbRow(JsonTokenType jsonTokenType, int location, int sizeOrLength)
+            {
+                Debug.Assert(jsonTokenType > JsonTokenType.None && jsonTokenType <= JsonTokenType.Comment);
+                Debug.Assert((byte)jsonTokenType < 1 << 4);
+                Debug.Assert(location >= 0);
+                Debug.Assert(sizeOrLength >= UnknownSize);
+
+                _location = location;
+                _sizeOrLengthUnion = sizeOrLength;
+                _numberOfRowsAndTypeUnion = (int)jsonTokenType << 28;
+            }
+
+            internal bool IsSimpleValue => TokenType >= JsonTokenType.PropertyName;
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.MetadataDb.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.MetadataDb.cs
new file mode 100644 (file)
index 0000000..b87e81e
--- /dev/null
@@ -0,0 +1,264 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Buffers;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace System.Text.Json
+{
+    public sealed partial class JsonDocument
+    {
+        // The database for the parsed structure of a JSON document.
+        //
+        // Every token from the document gets a row, which has one of the following forms:
+        //
+        // Number
+        // * First int
+        //   * Top bit is unassigned / always clear
+        //   * 31 bits for token offset
+        // * Second int
+        //   * Top bit is set if the number uses scientific notation
+        //   * 31 bits for the token length
+        // * Third int
+        //   * 4 bits JsonTokenType
+        //   * 28 bits unassigned / always clear
+        //
+        // String, PropertyName
+        // * First int
+        //   * Top bit is unassigned / always clear
+        //   * 31 bits for token offset
+        // * Second int
+        //   * Top bit is set if the string requires unescaping
+        //   * 31 bits for the token length
+        // * Third int
+        //   * 4 bits JsonTokenType
+        //   * 28 bits unassigned / always clear
+        //
+        // Other value types (True, False, Null)
+        // * First int
+        //   * Top bit is unassigned / always clear
+        //   * 31 bits for token offset
+        // * Second int
+        //   * Top bit is unassigned / always clear
+        //   * 31 bits for the token length
+        // * Third int
+        //   * 4 bits JsonTokenType
+        //   * 28 bits unassigned / always clear
+        //
+        // EndObject / EndArray
+        // * First int
+        //   * Top bit is unassigned / always clear
+        //   * 31 bits for token offset
+        // * Second int
+        //   * Top bit is unassigned / always clear
+        //   * 31 bits for the token length (always 1, effectively unassigned)
+        // * Third int
+        //   * 4 bits JsonTokenType
+        //   * 28 bits for the number of rows until the previous value (never 0)
+        //
+        // StartObject
+        // * First int
+        //   * Top bit is unassigned / always clear
+        //   * 31 bits for token offset
+        // * Second int
+        //   * Top bit is unassigned / always clear
+        //   * 31 bits for the token length (always 1, effectively unassigned)
+        // * Third int
+        //   * 4 bits JsonTokenType
+        //   * 28 bits for the number of rows until the next value (never 0)
+        //
+        // StartArray
+        // * First int
+        //   * Top bit is unassigned / always clear
+        //   * 31 bits for token offset
+        // * Second int
+        //   * Top bit is set if the array contains other arrays or objects ("complex" types)
+        //   * 31 bits for the number of elements in this array
+        // * Third int
+        //   * 4 bits JsonTokenType
+        //   * 28 bits for the number of rows until the next value (never 0)
+        private struct MetadataDb : IDisposable
+        {
+            private const int SizeOrLengthOffset = 4;
+            private const int NumberOfRowsOffset = 8;
+
+            internal int Length { get; private set; }
+            private byte[] _rentedBuffer;
+
+            internal MetadataDb(int payloadLength)
+            {
+                // Assume that a token happens approximately every 12 bytes.
+                // int estimatedTokens = payloadLength / 12
+                // now acknowledge that the number of bytes we need per token is 12.
+                // So that's just the payload length.
+                //
+                // Add one token's worth of data just because.
+                int initialSize = DbRow.Size + payloadLength;
+
+                // Stick with ArrayPool's rent/return range if it looks feasible.
+                // If it's wrong, we'll just grow and copy as we would if the tokens
+                // were more frequent anyways.
+                const int OneMegabyte = 1024 * 1024;
+
+                if (initialSize > OneMegabyte && initialSize <= 4 * OneMegabyte)
+                {
+                    initialSize = OneMegabyte;
+                }
+
+                _rentedBuffer = ArrayPool<byte>.Shared.Rent(initialSize);
+                Length = 0;
+            }
+
+            public void Dispose()
+            {
+                if (_rentedBuffer == null)
+                {
+                    return;
+                }
+
+                // The data in this rented buffer only conveys the positions and
+                // lengths of tokens in a document, but no content; so it does not
+                // need to be cleared.
+                ArrayPool<byte>.Shared.Return(_rentedBuffer);
+                _rentedBuffer = null;
+                Length = 0;
+            }
+
+            internal void TrimExcess()
+            {
+                // There's a chance that the size we have is the size we'd get for this
+                // amount of usage (particularly if Enlarge ever got called); and there's
+                // the small copy-cost associated with trimming anyways. "Is half-empty" is
+                // just a rough metric for "is trimming worth it?".
+                if (Length <= _rentedBuffer.Length / 2)
+                {
+                    byte[] newRent = ArrayPool<byte>.Shared.Rent(Length);
+                    byte[] returnBuf = newRent;
+
+                    if (newRent.Length < _rentedBuffer.Length)
+                    {
+                        Buffer.BlockCopy(_rentedBuffer, 0, newRent, 0, Length);
+                        returnBuf = _rentedBuffer;
+                        _rentedBuffer = newRent;
+                    }
+
+                    // The data in this rented buffer only conveys the positions and
+                    // lengths of tokens in a document, but no content; so it does not
+                    // need to be cleared.
+                    ArrayPool<byte>.Shared.Return(returnBuf);
+                }
+            }
+
+            internal void Append(JsonTokenType tokenType, int startLocation, int length)
+            {
+                // StartArray or StartObject should have length -1, otherwise the length should not be -1.
+                Debug.Assert(
+                    (tokenType == JsonTokenType.StartArray || tokenType == JsonTokenType.StartObject) ==
+                    (length == DbRow.UnknownSize));
+
+                if (Length >= _rentedBuffer.Length - DbRow.Size)
+                {
+                    Enlarge();
+                }
+
+                DbRow row = new DbRow(tokenType, startLocation, length);
+                MemoryMarshal.Write(_rentedBuffer.AsSpan(Length), ref row);
+                Length += DbRow.Size;
+            }
+
+            private void Enlarge()
+            {
+                byte[] toReturn = _rentedBuffer;
+                _rentedBuffer = ArrayPool<byte>.Shared.Rent(toReturn.Length * 2);
+                Buffer.BlockCopy(toReturn, 0, _rentedBuffer, 0, toReturn.Length);
+
+                // The data in this rented buffer only conveys the positions and
+                // lengths of tokens in a document, but no content; so it does not
+                // need to be cleared.
+                ArrayPool<byte>.Shared.Return(toReturn);
+            }
+
+            [Conditional("DEBUG")]
+            private void AssertValidIndex(int index)
+            {
+                Debug.Assert(index >= 0);
+                Debug.Assert(index <= Length - DbRow.Size, $"index {index} is out of bounds");
+                Debug.Assert(index % DbRow.Size == 0, $"index {index} is not at a record start position");
+            }
+
+            internal void SetLength(int index, int length)
+            {
+                AssertValidIndex(index);
+                Debug.Assert(length >= 0);
+                Span<byte> destination = _rentedBuffer.AsSpan(index + SizeOrLengthOffset);
+                MemoryMarshal.Write(destination, ref length);
+            }
+
+            internal void SetNumberOfRows(int index, int numberOfRows)
+            {
+                AssertValidIndex(index);
+                Debug.Assert(numberOfRows >= 1 && numberOfRows <= 0x0FFFFFFF);
+
+                Span<byte> dataPos = _rentedBuffer.AsSpan(index + NumberOfRowsOffset);
+                int current = MemoryMarshal.Read<int>(dataPos);
+
+                // Persist the most significant nybble
+                int value = (current & unchecked((int)0xF0000000)) | numberOfRows;
+                MemoryMarshal.Write(dataPos, ref value);
+            }
+
+            internal void SetHasComplexChildren(int index)
+            {
+                AssertValidIndex(index);
+
+                // The HasComplexChildren bit is the most significant bit of "SizeOrLength"
+                Span<byte> dataPos = _rentedBuffer.AsSpan(index + SizeOrLengthOffset);
+                int current = MemoryMarshal.Read<int>(dataPos);
+
+                int value = current | unchecked((int)0x80000000);
+                MemoryMarshal.Write(dataPos, ref value);
+            }
+
+            internal int FindIndexOfFirstUnsetSizeOrLength(JsonTokenType lookupType)
+            {
+                Debug.Assert(lookupType == JsonTokenType.StartObject || lookupType == JsonTokenType.StartArray);
+                return FindOpenElement(lookupType);
+            }
+
+            private int FindOpenElement(JsonTokenType lookupType)
+            {
+                Span<byte> data = _rentedBuffer.AsSpan(0, Length);
+
+                for (int i = Length - DbRow.Size; i >= 0; i -= DbRow.Size)
+                {
+                    DbRow row = MemoryMarshal.Read<DbRow>(data.Slice(i));
+
+                    if (row.IsUnknownSize && row.TokenType == lookupType)
+                    {
+                        return i;
+                    }
+                }
+
+                // We should never reach here.
+                Debug.Fail($"Unable to find expected {lookupType} token");
+                return -1;
+            }
+
+            internal DbRow Get(int index)
+            {
+                AssertValidIndex(index);
+                return MemoryMarshal.Read<DbRow>(_rentedBuffer.AsSpan(index));
+            }
+
+            internal JsonTokenType GetJsonTokenType(int index)
+            {
+                AssertValidIndex(index);
+                uint union = MemoryMarshal.Read<uint>(_rentedBuffer.AsSpan(index + NumberOfRowsOffset));
+
+                return (JsonTokenType)(union >> 28);
+            }
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.Parse.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.Parse.cs
new file mode 100644 (file)
index 0000000..9267342
--- /dev/null
@@ -0,0 +1,292 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Buffers;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Text.Json
+{
+    public sealed partial class JsonDocument
+    {
+        private const int UnseekableStreamInitialRentSize = 4096;
+
+        public static JsonDocument Parse(ReadOnlyMemory<byte> utf8Json, JsonReaderOptions readerOptions = default)
+        {
+            CheckSupportedOptions(readerOptions);
+
+            return Parse(utf8Json, readerOptions, null);
+        }
+
+        public static JsonDocument Parse(ReadOnlySequence<byte> utf8Json, JsonReaderOptions readerOptions = default)
+        {
+            CheckSupportedOptions(readerOptions);
+
+            if (utf8Json.IsSingleSegment)
+            {
+                return Parse(utf8Json.First, readerOptions, null);
+            }
+
+            int length = checked((int)utf8Json.Length);
+            byte[] utf8Bytes = ArrayPool<byte>.Shared.Rent(length);
+
+            try
+            {
+                utf8Json.CopyTo(utf8Bytes.AsSpan());
+                return Parse(utf8Bytes.AsMemory(0, length), readerOptions, utf8Bytes);
+            }
+            catch
+            {
+                // Holds document content, clear it before returning it.
+                utf8Bytes.AsSpan(0, length).Clear();
+                ArrayPool<byte>.Shared.Return(utf8Bytes);
+                throw;
+            }
+        }
+
+        public static JsonDocument Parse(Stream utf8Json, JsonReaderOptions readerOptions = default)
+        {
+            if (utf8Json == null)
+            {
+                throw new ArgumentNullException(nameof(utf8Json));
+            }
+
+            CheckSupportedOptions(readerOptions);
+
+            ArraySegment<byte> drained = ReadToEnd(utf8Json);
+
+            try
+            {
+                return Parse(drained.AsMemory(), readerOptions, drained.Array);
+            }
+            catch
+            {
+                // Holds document content, clear it before returning it.
+                drained.AsSpan().Clear();
+                ArrayPool<byte>.Shared.Return(drained.Array);
+                throw;
+            }
+        }
+
+        public static Task<JsonDocument> ParseAsync(
+            Stream utf8Json,
+            JsonReaderOptions readerOptions = default,
+            CancellationToken cancellationToken = default)
+        {
+            if (utf8Json == null)
+            {
+                throw new ArgumentNullException(nameof(utf8Json));
+            }
+
+            CheckSupportedOptions(readerOptions);
+
+            return ParseAsyncCore(utf8Json, readerOptions, cancellationToken);
+        }
+
+        private static async Task<JsonDocument> ParseAsyncCore(
+            Stream utf8Json,
+            JsonReaderOptions readerOptions = default,
+            CancellationToken cancellationToken = default)
+        {
+            ArraySegment<byte> drained = await ReadToEndAsync(utf8Json, cancellationToken).ConfigureAwait(false);
+
+            try
+            {
+                return Parse(drained.AsMemory(), readerOptions, drained.Array);
+            }
+            catch
+            {
+                // Holds document content, clear it before returning it.
+                drained.AsSpan().Clear();
+                ArrayPool<byte>.Shared.Return(drained.Array);
+                throw;
+            }
+        }
+
+        public static JsonDocument Parse(ReadOnlyMemory<char> json, JsonReaderOptions readerOptions = default)
+        {
+            CheckSupportedOptions(readerOptions);
+
+            ReadOnlySpan<char> jsonChars = json.Span;
+            int expectedByteCount = JsonReaderHelper.GetUtf8ByteCount(jsonChars);
+            byte[] utf8Bytes = ArrayPool<byte>.Shared.Rent(expectedByteCount);
+
+            try
+            {
+                int actualByteCount = JsonReaderHelper.GetUtf8FromText(jsonChars, utf8Bytes);
+                Debug.Assert(expectedByteCount == actualByteCount);
+
+                return Parse(utf8Bytes.AsMemory(0, actualByteCount), readerOptions, utf8Bytes);
+            }
+            catch
+            {
+                // Holds document content, clear it before returning it.
+                utf8Bytes.AsSpan(0, expectedByteCount).Clear();
+                ArrayPool<byte>.Shared.Return(utf8Bytes);
+                throw;
+            }
+        }
+
+        public static JsonDocument Parse(string json, JsonReaderOptions readerOptions = default)
+        {
+            if (json == null)
+            {
+                throw new ArgumentNullException(nameof(json));
+            }
+
+            CheckSupportedOptions(readerOptions);
+
+            return Parse(json.AsMemory(), readerOptions);
+        }
+
+        private static JsonDocument Parse(
+            ReadOnlyMemory<byte> utf8Json,
+            JsonReaderOptions readerOptions,
+            byte[] extraRentedBytes)
+        {
+            ReadOnlySpan<byte> utf8JsonSpan = utf8Json.Span;
+            Utf8JsonReader reader = new Utf8JsonReader(
+                utf8JsonSpan,
+                isFinalBlock: true,
+                new JsonReaderState(options: readerOptions));
+
+            var database = new MetadataDb(utf8Json.Length);
+            var stack = new StackRowStack(JsonReaderState.DefaultMaxDepth * StackRow.Size);
+
+            try
+            {
+                Parse(utf8JsonSpan, reader, ref database, ref stack);
+            }
+            catch
+            {
+                database.Dispose();
+                throw;
+            }
+            finally
+            {
+                stack.Dispose();
+            }
+
+            return new JsonDocument(utf8Json, database, extraRentedBytes);
+        }
+
+        private static ArraySegment<byte> ReadToEnd(Stream stream)
+        {
+            int written = 0;
+            byte[] rented = null;
+
+            try
+            {
+                long expectedLength = 0;
+
+                if (stream.CanSeek)
+                {
+                    expectedLength = Math.Max(1L, stream.Length - stream.Position);
+                    rented = ArrayPool<byte>.Shared.Rent(checked((int)expectedLength));
+                }
+                else
+                {
+                    rented = ArrayPool<byte>.Shared.Rent(UnseekableStreamInitialRentSize);
+                }
+
+                int lastRead;
+
+                do
+                {
+                    if (expectedLength == 0 && rented.Length == written)
+                    {
+                        byte[] toReturn = rented;
+                        rented = ArrayPool<byte>.Shared.Rent(checked(toReturn.Length * 2));
+                        Buffer.BlockCopy(toReturn, 0, rented, 0, toReturn.Length);
+                        // Holds document content, clear it.
+                        ArrayPool<byte>.Shared.Return(toReturn, clearArray: true);
+                    }
+
+                    lastRead = stream.Read(rented, written, rented.Length - written);
+                    written += lastRead;
+                } while (lastRead > 0);
+
+                return new ArraySegment<byte>(rented, 0, written);
+            }
+            catch
+            {
+                if (rented != null)
+                {
+                    // Holds document content, clear it before returning it.
+                    rented.AsSpan(0, written).Clear();
+                    ArrayPool<byte>.Shared.Return(rented);
+                }
+
+                throw;
+            }
+        }
+
+        private static async
+#if BUILDING_INBOX_LIBRARY
+            ValueTask<ArraySegment<byte>>
+#else
+            Task<ArraySegment<byte>>
+#endif
+            ReadToEndAsync(
+            Stream stream,
+            CancellationToken cancellationToken)
+        {
+            int written = 0;
+            byte[] rented = null;
+
+            try
+            {
+                long expectedLength = 0;
+
+                if (stream.CanSeek)
+                {
+                    expectedLength = Math.Max(1L, stream.Length - stream.Position);
+                    rented = ArrayPool<byte>.Shared.Rent(checked((int)expectedLength));
+                }
+                else
+                {
+                    rented = ArrayPool<byte>.Shared.Rent(UnseekableStreamInitialRentSize);
+                }
+
+                int lastRead;
+
+                do
+                {
+                    if (expectedLength == 0 && rented.Length == written)
+                    {
+                        byte[] toReturn = rented;
+                        rented = ArrayPool<byte>.Shared.Rent(toReturn.Length * 2);
+                        Buffer.BlockCopy(toReturn, 0, rented, 0, toReturn.Length);
+                        // Holds document content, clear it.
+                        ArrayPool<byte>.Shared.Return(toReturn, clearArray: true);
+                    }
+
+                    lastRead = await stream.ReadAsync(
+                        rented,
+                        written,
+                        rented.Length - written,
+                        cancellationToken).ConfigureAwait(false);
+
+                    written += lastRead;
+
+                } while (lastRead > 0);
+
+                return new ArraySegment<byte>(rented, 0, written);
+            }
+            catch
+            {
+                if (rented != null)
+                {
+                    // Holds document content, clear it before returning it.
+                    rented.AsSpan(0, written).Clear();
+                    ArrayPool<byte>.Shared.Return(rented);
+                }
+
+                throw;
+            }
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.StackRow.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.StackRow.cs
new file mode 100644 (file)
index 0000000..e4d1ae6
--- /dev/null
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace System.Text.Json
+{
+    public sealed partial class JsonDocument
+    {
+        // SizeOrLength - offset - 0 - size - 4
+        // NumberOfRows - offset - 4 - size - 4
+        [StructLayout(LayoutKind.Sequential)]
+        private struct StackRow
+        {
+            internal const int Size = 8;
+
+            internal int SizeOrLength;
+            internal int NumberOfRows;
+
+            internal StackRow(int sizeOrLength = 0, int numberOfRows = -1)
+            {
+                Debug.Assert(sizeOrLength >= 0);
+                Debug.Assert(numberOfRows >= -1);
+
+                SizeOrLength = sizeOrLength;
+                NumberOfRows = numberOfRows;
+            }
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.StackRowStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.StackRowStack.cs
new file mode 100644 (file)
index 0000000..abff741
--- /dev/null
@@ -0,0 +1,77 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Buffers;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace System.Text.Json
+{
+    public sealed partial class JsonDocument
+    {
+        private struct StackRowStack : IDisposable
+        {
+            private byte[] _rentedBuffer;
+            private int _topOfStack;
+
+            internal StackRowStack(int initialSize)
+            {
+                _rentedBuffer = ArrayPool<byte>.Shared.Rent(initialSize);
+                _topOfStack = _rentedBuffer.Length;
+            }
+
+            public void Dispose()
+            {
+                byte[] toReturn = _rentedBuffer;
+                _rentedBuffer = null;
+                _topOfStack = 0;
+
+                if (toReturn != null)
+                {
+                    // The data in this rented buffer only conveys the positions and
+                    // lengths of tokens in a document, but no content; so it does not
+                    // need to be cleared.
+                    ArrayPool<byte>.Shared.Return(toReturn);
+                }
+            }
+
+            internal void Push(StackRow row)
+            {
+                if (_topOfStack < StackRow.Size)
+                {
+                    Enlarge();
+                }
+
+                _topOfStack -= StackRow.Size;
+                MemoryMarshal.Write(_rentedBuffer.AsSpan(_topOfStack), ref row);
+            }
+
+            internal StackRow Pop()
+            {
+                Debug.Assert(_topOfStack <= _rentedBuffer.Length - StackRow.Size);
+                StackRow row = MemoryMarshal.Read<StackRow>(_rentedBuffer.AsSpan(_topOfStack));
+                _topOfStack += StackRow.Size;
+                return row;
+            }
+
+            private void Enlarge()
+            {
+                byte[] toReturn = _rentedBuffer;
+                _rentedBuffer = ArrayPool<byte>.Shared.Rent(toReturn.Length * 2);
+
+                Buffer.BlockCopy(
+                    toReturn,
+                    _topOfStack,
+                    _rentedBuffer,
+                    _rentedBuffer.Length - toReturn.Length + _topOfStack,
+                    toReturn.Length - _topOfStack);
+
+                // The data in this rented buffer only conveys the positions and
+                // lengths of tokens in a document, but no content; so it does not
+                // need to be cleared.
+                ArrayPool<byte>.Shared.Return(toReturn);
+            }
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.TryGetProperty.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.TryGetProperty.cs
new file mode 100644 (file)
index 0000000..54ebb1c
--- /dev/null
@@ -0,0 +1,228 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Buffers;
+using System.Diagnostics;
+
+namespace System.Text.Json
+{
+    public sealed partial class JsonDocument
+    {
+        internal bool TryGetNamedPropertyValue(int index, ReadOnlySpan<char> propertyName, out JsonElement value)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(index);
+
+            CheckExpectedType(JsonTokenType.StartObject, row.TokenType);
+
+            // Only one row means it was EndObject.
+            if (row.NumberOfRows == 1)
+            {
+                value = default;
+                return false;
+            }
+
+            int maxBytes = JsonReaderHelper.s_utf8Encoding.GetMaxByteCount(propertyName.Length);
+            int startIndex = index + DbRow.Size;
+            int endIndex = checked(row.NumberOfRows * DbRow.Size + index);
+
+            if (maxBytes < JsonConstants.StackallocThreshold)
+            {
+                Span<byte> utf8Name = stackalloc byte[JsonConstants.StackallocThreshold];
+                int len = JsonReaderHelper.GetUtf8FromText(propertyName, utf8Name);
+                utf8Name = utf8Name.Slice(0, len);
+
+                return TryGetNamedPropertyValue(
+                    startIndex,
+                    endIndex,
+                    utf8Name,
+                    out value);
+            }
+
+            // Unescaping the property name will make the string shorter (or the same)
+            // So the first viable candidate is one whose length in bytes matches, or
+            // exceeds, our length in chars.
+            //
+            // The maximal escaping seems to be 6 -> 1 ("\u0030" => "0"), but just transcode
+            // and switch once one viable long property is found.
+
+            int minBytes = propertyName.Length;
+            // Move to the row before the EndObject
+            int candidateIndex = endIndex - DbRow.Size;
+
+            while (candidateIndex > index)
+            {
+                int passedIndex = candidateIndex;
+
+                row = _parsedData.Get(candidateIndex);
+                Debug.Assert(row.TokenType != JsonTokenType.PropertyName);
+
+                // Move before the value
+                if (row.IsSimpleValue)
+                {
+                    candidateIndex -= DbRow.Size;
+                }
+                else
+                {
+                    Debug.Assert(row.NumberOfRows > 0);
+                    candidateIndex -= DbRow.Size * (row.NumberOfRows + 1);
+                }
+
+                row = _parsedData.Get(candidateIndex);
+                Debug.Assert(row.TokenType == JsonTokenType.PropertyName);
+
+                if (row.SizeOrLength >= minBytes)
+                {
+                    byte[] tmpUtf8 = ArrayPool<byte>.Shared.Rent(maxBytes);
+                    Span<byte> utf8Name = default;
+
+                    try
+                    {
+                        int len = JsonReaderHelper.GetUtf8FromText(propertyName, tmpUtf8);
+                        utf8Name = tmpUtf8.AsSpan(0, len);
+
+                        return TryGetNamedPropertyValue(
+                            startIndex,
+                            passedIndex + DbRow.Size,
+                            utf8Name,
+                            out value);
+                    }
+                    finally
+                    {
+                        // While property names aren't usually a secret, they also usually
+                        // aren't long enough to end up in the rented buffer transcode path.
+                        //
+                        // On the basis that this is user data, go ahead and clear it.
+                        utf8Name.Clear();
+                        ArrayPool<byte>.Shared.Return(tmpUtf8);
+                    }
+                }
+
+                // Move to the previous value
+                candidateIndex -= DbRow.Size;
+            }
+
+            // None of the property names were within the range that the UTF-8 encoding would have been.
+            value = default;
+            return false;
+        }
+
+        internal bool TryGetNamedPropertyValue(int index, ReadOnlySpan<byte> propertyName, out JsonElement value)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(index);
+
+            CheckExpectedType(JsonTokenType.StartObject, row.TokenType);
+
+            // Only one row means it was EndObject.
+            if (row.NumberOfRows == 1)
+            {
+                value = default;
+                return false;
+            }
+
+            int endIndex = checked(row.NumberOfRows * DbRow.Size + index);
+
+            return TryGetNamedPropertyValue(
+                index + DbRow.Size,
+                endIndex,
+                propertyName,
+                out value);
+        }
+
+        private bool TryGetNamedPropertyValue(
+            int startIndex,
+            int endIndex,
+            ReadOnlySpan<byte> propertyName,
+            out JsonElement value)
+        {
+            ReadOnlySpan<byte> documentSpan = _utf8Json.Span;
+
+            // Move to the row before the EndObject
+            int index = endIndex - DbRow.Size;
+
+            while (index > startIndex)
+            {
+                DbRow row = _parsedData.Get(index);
+                Debug.Assert(row.TokenType != JsonTokenType.PropertyName);
+
+                // Move before the value
+                if (row.IsSimpleValue)
+                {
+                    index -= DbRow.Size;
+                }
+                else
+                {
+                    Debug.Assert(row.NumberOfRows > 0);
+                    index -= DbRow.Size * (row.NumberOfRows + 1);
+                }
+
+                row = _parsedData.Get(index);
+                Debug.Assert(row.TokenType == JsonTokenType.PropertyName);
+
+                ReadOnlySpan<byte> currentPropertyName = documentSpan.Slice(row.Location, row.SizeOrLength);
+
+                if (row.HasComplexChildren)
+                {
+                    // An escaped property name will be longer than an unescaped candidate, so only unescape
+                    // when the lengths are compatible.
+                    if (currentPropertyName.Length > propertyName.Length)
+                    {
+                        int idx = currentPropertyName.IndexOf(JsonConstants.BackSlash);
+                        Debug.Assert(idx >= 0);
+
+                        // If everything up to where the property name has a backslash matches, keep going.
+                        if (propertyName.Length > idx &&
+                            currentPropertyName.Slice(0, idx).SequenceEqual(propertyName.Slice(0, idx)))
+                        {
+                            int remaining = currentPropertyName.Length - idx;
+                            int written = 0;
+                            byte[] rented = null;
+                            
+                            try
+                            {
+                                Span<byte> utf8Unescaped = remaining <= JsonConstants.StackallocThreshold ?
+                                    stackalloc byte[remaining] :
+                                    (rented = ArrayPool<byte>.Shared.Rent(remaining));
+
+                                // Only unescape the part we haven't processed.
+                                JsonReaderHelper.Unescape(currentPropertyName.Slice(idx), utf8Unescaped, 0, out written);
+
+                                // If the unescaped remainder matches the input remainder, it's a match.
+                                if (utf8Unescaped.Slice(0, written).SequenceEqual(propertyName.Slice(idx)))
+                                {
+                                    // If the property name is a match, the answer is the next element.
+                                    value = new JsonElement(this, index + DbRow.Size);
+                                    return true;
+                                }
+                            }
+                            finally
+                            {
+                                if (rented != null)
+                                {
+                                    rented.AsSpan(0, written).Clear();
+                                    ArrayPool<byte>.Shared.Return(rented);
+                                }
+                            }
+                        }
+                    }
+                }
+                else if (currentPropertyName.SequenceEqual(propertyName))
+                {
+                    // If the property name is a match, the answer is the next element.
+                    value = new JsonElement(this, index + DbRow.Size);
+                    return true;
+                }
+
+                // Move to the previous value
+                index -= DbRow.Size;
+            }
+
+            value = default;
+            return false;
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonDocument.cs
new file mode 100644 (file)
index 0000000..e24adda
--- /dev/null
@@ -0,0 +1,594 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Buffers;
+using System.Buffers.Text;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+#if BUILDING_INBOX_LIBRARY
+using Internal.Runtime.CompilerServices;
+#else
+using System.Runtime.CompilerServices;
+#endif
+
+namespace System.Text.Json
+{
+    public sealed partial class JsonDocument : IDisposable
+    {
+        private ReadOnlyMemory<byte> _utf8Json;
+        private MetadataDb _parsedData;
+        private byte[] _extraRentedBytes;
+        private (int, string) _lastIndexAndString = (-1, null);
+
+        public JsonElement RootElement => new JsonElement(this, 0);
+
+        private JsonDocument(ReadOnlyMemory<byte> utf8Json, MetadataDb parsedData, byte[] extraRentedBytes)
+        {
+            Debug.Assert(!utf8Json.IsEmpty);
+
+            _utf8Json = utf8Json;
+            _parsedData = parsedData;
+            _extraRentedBytes = extraRentedBytes;
+        }
+
+        public void Dispose()
+        {
+            if (_utf8Json.IsEmpty)
+            {
+                return;
+            }
+
+            int length = _utf8Json.Length;
+            _utf8Json = ReadOnlyMemory<byte>.Empty;
+            _parsedData.Dispose();
+
+            // When "extra rented bytes exist" they contain the document,
+            // and thus need to be cleared before being returned.
+            if (_extraRentedBytes != null)
+            {
+                _extraRentedBytes.AsSpan(0, length).Clear();
+                ArrayPool<byte>.Shared.Return(_extraRentedBytes);
+                _extraRentedBytes = null;
+            }
+        }
+
+        internal JsonTokenType GetJsonTokenType(int index)
+        {
+            CheckNotDisposed();
+
+            return _parsedData.GetJsonTokenType(index);
+        }
+
+        internal int GetArrayLength(int index)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(index);
+
+            CheckExpectedType(JsonTokenType.StartArray, row.TokenType);
+
+            return row.SizeOrLength;
+        }
+
+        internal JsonElement GetArrayIndexElement(int currentIndex, int arrayIndex)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(currentIndex);
+
+            CheckExpectedType(JsonTokenType.StartArray, row.TokenType);
+
+            int arrayLength = row.SizeOrLength;
+
+            if ((uint)arrayIndex >= (uint)arrayLength)
+            {
+                throw new IndexOutOfRangeException();
+            }
+
+            if (!row.HasComplexChildren)
+            {
+                // Since we wouldn't be here without having completed the document parse, and we
+                // already vetted the index against the length, this new index will always be
+                // within the table.
+                return new JsonElement(this, currentIndex + ((arrayIndex + 1) * DbRow.Size));
+            }
+
+            int elementCount = 0;
+            int objectOffset = currentIndex + DbRow.Size;
+
+            for (; objectOffset < _parsedData.Length; objectOffset += DbRow.Size)
+            {
+                if (arrayIndex == elementCount)
+                {
+                    return new JsonElement(this, objectOffset);
+                }
+
+                row = _parsedData.Get(objectOffset);
+
+                if (!row.IsSimpleValue)
+                {
+                    objectOffset += DbRow.Size * row.NumberOfRows;
+                }
+
+                elementCount++;
+            }
+
+            Debug.Fail(
+                $"Ran out of database searching for array index {arrayIndex} from {currentIndex} when length was {arrayLength}");
+            throw new IndexOutOfRangeException();
+        }
+
+        internal int GetEndIndex(int index, bool includeEndElement)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(index);
+
+            if (row.IsSimpleValue)
+            {
+                return index + DbRow.Size;
+            }
+
+            int endIndex = index + DbRow.Size * row.NumberOfRows;
+
+            if (includeEndElement)
+            {
+                endIndex += DbRow.Size;
+            }
+
+            return endIndex;
+        }
+
+        private ReadOnlyMemory<byte> GetRawValue(int index, bool includeQuotes)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(index);
+
+            if (row.IsSimpleValue)
+            {
+                if (includeQuotes && row.TokenType == JsonTokenType.String)
+                {
+                    // Start one character earlier than the value (the open quote)
+                    // End one character after the value (the close quote)
+                    return _utf8Json.Slice(row.Location - 1, row.SizeOrLength + 2);
+                }
+
+                return _utf8Json.Slice(row.Location, row.SizeOrLength);
+            }
+
+            int endElementIdx = GetEndIndex(index, includeEndElement: false);
+            int start = row.Location;
+            row = _parsedData.Get(endElementIdx);
+            return _utf8Json.Slice(start, row.Location - start + row.SizeOrLength);
+        }
+
+        private ReadOnlyMemory<byte> GetPropertyRawValue(int valueIndex)
+        {
+            CheckNotDisposed();
+
+            // The property name is stored one row before the value
+            DbRow row = _parsedData.Get(valueIndex - DbRow.Size);
+            Debug.Assert(row.TokenType == JsonTokenType.PropertyName);
+
+            // Subtract one for the open quote.
+            int start = row.Location - 1;
+            int end;
+
+            row = _parsedData.Get(valueIndex);
+
+            if (row.IsSimpleValue)
+            {
+                end = row.Location + row.SizeOrLength;
+
+                // If the value was a string, pick up the terminating quote.
+                if (row.TokenType == JsonTokenType.String)
+                {
+                    end++;
+                }
+
+                return _utf8Json.Slice(start, end - start);
+            }
+
+            int endElementIdx = GetEndIndex(valueIndex, includeEndElement: false);
+            row = _parsedData.Get(endElementIdx);
+            end = row.Location + row.SizeOrLength;
+            return _utf8Json.Slice(start, end - start);
+        }
+
+        internal string GetString(int index, JsonTokenType expectedType)
+        {
+            CheckNotDisposed();
+
+            (int lastIdx, string lastString) = _lastIndexAndString;
+
+            if (lastIdx == index)
+            {
+                return lastString;
+            }
+
+            DbRow row = _parsedData.Get(index);
+
+            JsonTokenType tokenType = row.TokenType;
+
+            if (tokenType == JsonTokenType.Null)
+            {
+                return null;
+            }
+
+            CheckExpectedType(expectedType, tokenType);
+
+            ReadOnlySpan<byte> data = _utf8Json.Span;
+            ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);
+
+            if (row.HasComplexChildren)
+            {
+                int backslash = segment.IndexOf(JsonConstants.BackSlash);
+                lastString = JsonReaderHelper.GetUnescapedString(segment, backslash);
+            }
+            else
+            {
+                lastString = JsonReaderHelper.TranscodeHelper(segment);
+            }
+
+            _lastIndexAndString = (index, lastString);
+            return lastString;
+        }
+
+        internal string GetNameOfPropertyValue(int index)
+        {
+            // The property name is one row before the property value
+            return GetString(index - DbRow.Size, JsonTokenType.PropertyName);
+        }
+
+        internal bool TryGetValue(int index, out int value)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(index);
+
+            CheckExpectedType(JsonTokenType.Number, row.TokenType);
+
+            ReadOnlySpan<byte> data = _utf8Json.Span;
+            ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);
+
+            if (Utf8Parser.TryParse(segment, out int tmp, out int consumed) &&
+                consumed == segment.Length)
+            {
+                value = tmp;
+                return true;
+            }
+
+            value = default;
+            return false;
+        }
+
+        internal bool TryGetValue(int index, out uint value)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(index);
+
+            CheckExpectedType(JsonTokenType.Number, row.TokenType);
+
+            ReadOnlySpan<byte> data = _utf8Json.Span;
+            ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);
+
+            if (Utf8Parser.TryParse(segment, out uint tmp, out int consumed) &&
+                consumed == segment.Length)
+            {
+                value = tmp;
+                return true;
+            }
+
+            value = default;
+            return false;
+        }
+
+        internal bool TryGetValue(int index, out long value)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(index);
+
+            CheckExpectedType(JsonTokenType.Number, row.TokenType);
+
+            ReadOnlySpan<byte> data = _utf8Json.Span;
+            ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);
+
+            if (Utf8Parser.TryParse(segment, out long tmp, out int consumed) &&
+                consumed == segment.Length)
+            {
+                value = tmp;
+                return true;
+            }
+
+            value = default;
+            return false;
+        }
+
+        internal bool TryGetValue(int index, out ulong value)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(index);
+
+            CheckExpectedType(JsonTokenType.Number, row.TokenType);
+
+            ReadOnlySpan<byte> data = _utf8Json.Span;
+            ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);
+
+            if (Utf8Parser.TryParse(segment, out ulong tmp, out int consumed) &&
+                consumed == segment.Length)
+            {
+                value = tmp;
+                return true;
+            }
+
+            value = default;
+            return false;
+        }
+
+        internal bool TryGetValue(int index, out double value)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(index);
+
+            CheckExpectedType(JsonTokenType.Number, row.TokenType);
+
+            ReadOnlySpan<byte> data = _utf8Json.Span;
+            ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);
+
+            char standardFormat = row.HasComplexChildren ? JsonConstants.ScientificNotationFormat : default;
+
+            if (Utf8Parser.TryParse(segment, out double tmp, out int bytesConsumed, standardFormat) &&
+                segment.Length == bytesConsumed)
+            {
+                value = tmp;
+                return true;
+            }
+
+            value = default;
+            return false;
+        }
+
+        internal bool TryGetValue(int index, out float value)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(index);
+
+            CheckExpectedType(JsonTokenType.Number, row.TokenType);
+
+            ReadOnlySpan<byte> data = _utf8Json.Span;
+            ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);
+
+            char standardFormat = row.HasComplexChildren ? JsonConstants.ScientificNotationFormat : default;
+
+            if (Utf8Parser.TryParse(segment, out float tmp, out int bytesConsumed, standardFormat) &&
+                segment.Length == bytesConsumed)
+            {
+                value = tmp;
+                return true;
+            }
+
+            value = default;
+            return false;
+        }
+
+        internal bool TryGetValue(int index, out decimal value)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(index);
+
+            CheckExpectedType(JsonTokenType.Number, row.TokenType);
+
+            ReadOnlySpan<byte> data = _utf8Json.Span;
+            ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);
+
+            char standardFormat = row.HasComplexChildren ? JsonConstants.ScientificNotationFormat : default;
+
+            if (Utf8Parser.TryParse(segment, out decimal tmp, out int bytesConsumed, standardFormat) &&
+                segment.Length == bytesConsumed)
+            {
+                value = tmp;
+                return true;
+            }
+
+            value = default;
+            return false;
+        }
+
+        internal string GetRawValueAsString(int index)
+        {
+            ReadOnlyMemory<byte> segment = GetRawValue(index, includeQuotes: true);
+            return JsonReaderHelper.TranscodeHelper(segment.Span);
+        }
+
+        internal string GetPropertyRawValueAsString(int valueIndex)
+        {
+            ReadOnlyMemory<byte> segment = GetPropertyRawValue(valueIndex);
+            return JsonReaderHelper.TranscodeHelper(segment.Span);
+        }
+
+        private static void Parse(
+            ReadOnlySpan<byte> utf8JsonSpan,
+            Utf8JsonReader reader,
+            ref MetadataDb database,
+            ref StackRowStack stack)
+        {
+            bool inArray = false;
+            int arrayItemsCount = 0;
+            int numberOfRowsForMembers = 0;
+            int numberOfRowsForValues = 0;
+
+            ref byte jsonStart = ref MemoryMarshal.GetReference(utf8JsonSpan);
+
+            while (reader.Read())
+            {
+                JsonTokenType tokenType = reader.TokenType;
+
+                int tokenStart = Unsafe.ByteOffset(
+                    ref jsonStart,
+                    ref MemoryMarshal.GetReference(reader.ValueSpan)).ToInt32();
+
+                if (tokenType == JsonTokenType.StartObject)
+                {
+                    if (inArray)
+                    {
+                        arrayItemsCount++;
+                    }
+
+                    numberOfRowsForValues++;
+                    database.Append(tokenType, tokenStart, DbRow.UnknownSize);
+                    var row = new StackRow(numberOfRowsForMembers + 1);
+                    stack.Push(row);
+                    numberOfRowsForMembers = 0;
+                }
+                else if (tokenType == JsonTokenType.EndObject)
+                {
+                    int rowIndex = database.FindIndexOfFirstUnsetSizeOrLength(JsonTokenType.StartObject);
+
+                    numberOfRowsForValues++;
+                    numberOfRowsForMembers++;
+                    database.SetLength(rowIndex, numberOfRowsForMembers);
+
+                    int newRowIndex = database.Length;
+                    database.Append(tokenType, tokenStart, reader.ValueSpan.Length);
+                    database.SetNumberOfRows(rowIndex, numberOfRowsForMembers);
+                    database.SetNumberOfRows(newRowIndex, numberOfRowsForMembers);
+
+                    StackRow row = stack.Pop();
+                    numberOfRowsForMembers += row.SizeOrLength;
+                }
+                else if (tokenType == JsonTokenType.StartArray)
+                {
+                    if (inArray)
+                    {
+                        arrayItemsCount++;
+                    }
+
+                    numberOfRowsForMembers++;
+                    database.Append(tokenType, tokenStart, DbRow.UnknownSize);
+                    var row = new StackRow(arrayItemsCount, numberOfRowsForValues + 1);
+                    stack.Push(row);
+                    arrayItemsCount = 0;
+                    numberOfRowsForValues = 0;
+                }
+                else if (tokenType == JsonTokenType.EndArray)
+                {
+                    int rowIndex = database.FindIndexOfFirstUnsetSizeOrLength(JsonTokenType.StartArray);
+
+                    numberOfRowsForValues++;
+                    numberOfRowsForMembers++;
+                    database.SetLength(rowIndex, arrayItemsCount);
+                    database.SetNumberOfRows(rowIndex, numberOfRowsForValues);
+
+                    // If the array item count is (e.g.) 12 and the number of rows is (e.g.) 13
+                    // then the extra row is just this EndArray item, so the array was made up
+                    // of simple values.
+                    //
+                    // If the off-by-one relationship does not hold, then one of the values was
+                    // more than one row, making it a complex object.
+                    //
+                    // This check is similar to tracking the start array and painting it when
+                    // StartObject or StartArray is encountered, but avoids the mixed state
+                    // where "UnknownSize" implies "has complex children".
+                    if (arrayItemsCount + 1 != numberOfRowsForValues)
+                    {
+                        database.SetHasComplexChildren(rowIndex);
+                    }
+
+                    int newRowIndex = database.Length;
+                    database.Append(tokenType, tokenStart, reader.ValueSpan.Length);
+                    database.SetNumberOfRows(newRowIndex, numberOfRowsForValues);
+
+                    StackRow row = stack.Pop();
+                    arrayItemsCount = row.SizeOrLength;
+                    numberOfRowsForValues += row.NumberOfRows;
+                }
+                else if (tokenType == JsonTokenType.PropertyName)
+                {
+                    numberOfRowsForValues++;
+                    numberOfRowsForMembers++;
+                    database.Append(tokenType, tokenStart, reader.ValueSpan.Length);
+
+                    if (reader._stringHasEscaping)
+                    {
+                        database.SetHasComplexChildren(database.Length - DbRow.Size);
+                    }
+
+                    Debug.Assert(!inArray);
+                }
+                else
+                {
+                    Debug.Assert(tokenType >= JsonTokenType.String && tokenType <= JsonTokenType.Null);
+                    numberOfRowsForValues++;
+                    numberOfRowsForMembers++;
+                    database.Append(tokenType, tokenStart, reader.ValueSpan.Length);
+
+                    if (inArray)
+                    {
+                        arrayItemsCount++;
+                    }
+
+                    if (tokenType == JsonTokenType.Number)
+                    {
+                        switch (reader._numberFormat)
+                        {
+                            case JsonConstants.ScientificNotationFormat:
+                                database.SetHasComplexChildren(database.Length - DbRow.Size);
+                                break;
+                            default:
+                                Debug.Assert(
+                                    reader._numberFormat == default,
+                                    $"Unhandled numeric format {reader._numberFormat}");
+                                break;
+                        }
+                    }
+                    else if (tokenType == JsonTokenType.String)
+                    {
+                        if (reader._stringHasEscaping)
+                        {
+                            database.SetHasComplexChildren(database.Length - DbRow.Size);
+                        }
+                    }
+                }
+
+                inArray = reader.IsInArray;
+            }
+
+            Debug.Assert(reader.BytesConsumed == utf8JsonSpan.Length);
+            database.TrimExcess();
+        }
+
+        private void CheckNotDisposed()
+        {
+            if (_utf8Json.IsEmpty)
+            {
+                throw new ObjectDisposedException(nameof(JsonDocument));
+            }
+        }
+
+        private void CheckExpectedType(JsonTokenType expected, JsonTokenType actual)
+        {
+            if (expected != actual)
+            {
+                throw ThrowHelper.GetJsonElementWrongTypeException(expected, actual);
+            }
+        }
+
+        private static void CheckSupportedOptions(JsonReaderOptions readerOptions)
+        {
+            if (readerOptions.CommentHandling == JsonCommentHandling.Allow)
+            {
+                throw new ArgumentException(
+                    SR.JsonDocumentDoesNotSupportComments,
+                    nameof(readerOptions));
+            }
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonElement.ArrayEnumerator.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonElement.ArrayEnumerator.cs
new file mode 100644 (file)
index 0000000..60bfa34
--- /dev/null
@@ -0,0 +1,74 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace System.Text.Json
+{
+    public partial struct JsonElement
+    {
+        public struct ArrayEnumerator : IEnumerable<JsonElement>, IEnumerator<JsonElement>
+        {
+            private readonly JsonElement _target;
+            private int _curIdx;
+            private readonly int _endIdx;
+
+            internal ArrayEnumerator(JsonElement target)
+            {
+                Debug.Assert(target.TokenType == JsonTokenType.StartArray);
+
+                _target = target;
+                _curIdx = -1;
+                _endIdx = _target._parent.GetEndIndex(_target._idx, includeEndElement: false);
+            }
+
+            public JsonElement Current =>
+                _curIdx < 0 ? default : new JsonElement(_target._parent, _curIdx);
+
+            public ArrayEnumerator GetEnumerator()
+            {
+                ArrayEnumerator ator = this;
+                ator._curIdx = -1;
+                return ator;
+            }
+
+            IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+            IEnumerator<JsonElement> IEnumerable<JsonElement>.GetEnumerator() => GetEnumerator();
+
+            public void Dispose()
+            {
+                _curIdx = _endIdx;
+            }
+
+            public void Reset()
+            {
+                _curIdx = -1;
+            }
+
+            object IEnumerator.Current => Current;
+
+            public bool MoveNext()
+            {
+                if (_curIdx >= _endIdx)
+                {
+                    return false;
+                }
+
+                if (_curIdx < 0)
+                {
+                    _curIdx = _target._idx + JsonDocument.DbRow.Size;
+                }
+                else
+                {
+                    _curIdx = _target._parent.GetEndIndex(_curIdx, includeEndElement: true);
+                }
+
+                return _curIdx < _endIdx;
+            }
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonElement.ObjectEnumerator.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonElement.ObjectEnumerator.cs
new file mode 100644 (file)
index 0000000..e89768c
--- /dev/null
@@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace System.Text.Json
+{
+    public partial struct JsonElement
+    {
+        public struct ObjectEnumerator : IEnumerable<JsonProperty>, IEnumerator<JsonProperty>
+        {
+            private readonly JsonElement _target;
+            private int _curIdx;
+            private readonly int _endIdx;
+
+            internal ObjectEnumerator(JsonElement target)
+            {
+                Debug.Assert(target.TokenType == JsonTokenType.StartObject);
+
+                _target = target;
+                _curIdx = -1;
+                _endIdx = _target._parent.GetEndIndex(_target._idx, includeEndElement: false);
+            }
+
+            public JsonProperty Current =>
+                _curIdx < 0 ?
+                    default :
+                    new JsonProperty(new JsonElement(_target._parent, _curIdx));
+
+            public ObjectEnumerator GetEnumerator()
+            {
+                ObjectEnumerator ator = this;
+                ator._curIdx = -1;
+                return ator;
+            }
+
+            IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+            IEnumerator<JsonProperty> IEnumerable<JsonProperty>.GetEnumerator() => GetEnumerator();
+
+            public void Dispose()
+            {
+                _curIdx = _endIdx;
+            }
+
+            public void Reset()
+            {
+                _curIdx = -1;
+            }
+
+            object IEnumerator.Current => Current;
+
+            public bool MoveNext()
+            {
+                if (_curIdx >= _endIdx)
+                {
+                    return false;
+                }
+
+                if (_curIdx < 0)
+                {
+                    _curIdx = _target._idx + JsonDocument.DbRow.Size;
+                }
+                else
+                {
+                    _curIdx = _target._parent.GetEndIndex(_curIdx, includeEndElement: true);
+                }
+
+                // _curIdx is now pointing at a property name, move one more to get the value
+                _curIdx += JsonDocument.DbRow.Size;
+
+                return _curIdx < _endIdx;
+            }
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonElement.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonElement.cs
new file mode 100644 (file)
index 0000000..dd5adf5
--- /dev/null
@@ -0,0 +1,332 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace System.Text.Json
+{
+    public readonly partial struct JsonElement
+    {
+        private readonly JsonDocument _parent;
+        private readonly int _idx;
+
+        internal JsonElement(JsonDocument parent, int idx)
+        {
+            // parent is usually not null, but the Current property
+            // on the enumerators (when initialized as `default`) can
+            // get here with a null.
+            Debug.Assert(idx >= 0);
+
+            _parent = parent;
+            _idx = idx;
+        }
+
+        internal JsonTokenType TokenType => _parent?.GetJsonTokenType(_idx) ?? JsonTokenType.None;
+
+        public JsonValueType Type => TokenType.ToValueType();
+
+        public JsonElement this[int index]
+        {
+            get
+            {
+                CheckValidInstance();
+
+                return _parent.GetArrayIndexElement(_idx, index);
+            }
+        }
+
+        public int GetArrayLength()
+        {
+            CheckValidInstance();
+
+            return _parent.GetArrayLength(_idx);
+        }
+
+        public JsonElement GetProperty(string propertyName)
+        {
+            if (propertyName == null)
+                throw new ArgumentNullException(nameof(propertyName));
+
+            if (TryGetProperty(propertyName, out JsonElement property))
+            {
+                return property;
+            }
+
+            throw new KeyNotFoundException();
+        }
+
+        public JsonElement GetProperty(ReadOnlySpan<char> propertyName)
+        {
+            if (TryGetProperty(propertyName, out JsonElement property))
+            {
+                return property;
+            }
+
+            throw new KeyNotFoundException();
+        }
+
+        public JsonElement GetProperty(ReadOnlySpan<byte> utf8PropertyName)
+        {
+            if (TryGetProperty(utf8PropertyName, out JsonElement property))
+            {
+                return property;
+            }
+
+            throw new KeyNotFoundException();
+        }
+
+        public bool TryGetProperty(string propertyName, out JsonElement value)
+        {
+            if (propertyName == null)
+                throw new ArgumentNullException(nameof(propertyName));
+
+            return TryGetProperty(propertyName.AsSpan(), out value);
+        }
+
+        public bool TryGetProperty(ReadOnlySpan<char> propertyName, out JsonElement value)
+        {
+            CheckValidInstance();
+
+            return _parent.TryGetNamedPropertyValue(_idx, propertyName, out value);
+        }
+
+        public bool TryGetProperty(ReadOnlySpan<byte> utf8PropertyName, out JsonElement value)
+        {
+            CheckValidInstance();
+
+            return _parent.TryGetNamedPropertyValue(_idx, utf8PropertyName, out value);
+        }
+
+        public bool GetBoolean()
+        {
+            // CheckValidInstance is redundant.  Asking for the type will
+            // return None, which then throws the same exception in the return statement.
+
+            JsonTokenType type = TokenType;
+
+            return
+                type == JsonTokenType.True ? true :
+                type == JsonTokenType.False ? false :
+                throw ThrowHelper.GetJsonElementWrongTypeException(nameof(Boolean), type);
+        }
+
+        public string GetString()
+        {
+            CheckValidInstance();
+
+            return _parent.GetString(_idx, JsonTokenType.String);
+        }
+
+        public bool TryGetInt32(out int value)
+        {
+            CheckValidInstance();
+
+            return _parent.TryGetValue(_idx, out value);
+        }
+
+        public int GetInt32()
+        {
+            if (TryGetInt32(out int value))
+            {
+                return value;
+            }
+
+            throw new FormatException();
+        }
+
+        [CLSCompliant(false)]
+        public bool TryGetUInt32(out uint value)
+        {
+            CheckValidInstance();
+
+            return _parent.TryGetValue(_idx, out value);
+        }
+
+        [CLSCompliant(false)]
+        public uint GetUInt32()
+        {
+            if (TryGetUInt32(out uint value))
+            {
+                return value;
+            }
+
+            throw new FormatException();
+        }
+
+        public bool TryGetInt64(out long value)
+        {
+            CheckValidInstance();
+
+            return _parent.TryGetValue(_idx, out value);
+        }
+
+        public long GetInt64()
+        {
+            if (TryGetInt64(out long value))
+            {
+                return value;
+            }
+
+            throw new FormatException();
+        }
+
+        [CLSCompliant(false)]
+        public bool TryGetUInt64(out ulong value)
+        {
+            CheckValidInstance();
+
+            return _parent.TryGetValue(_idx, out value);
+        }
+
+        [CLSCompliant(false)]
+        public ulong GetUInt64()
+        {
+            if (TryGetUInt64(out ulong value))
+            {
+                return value;
+            }
+
+            throw new FormatException();
+        }
+
+        public bool TryGetDouble(out double value)
+        {
+            CheckValidInstance();
+
+            return _parent.TryGetValue(_idx, out value);
+        }
+
+        public double GetDouble()
+        {
+            if (TryGetDouble(out double value))
+            {
+                return value;
+            }
+
+            throw new FormatException();
+        }
+
+        public bool TryGetSingle(out float value)
+        {
+            CheckValidInstance();
+
+            return _parent.TryGetValue(_idx, out value);
+        }
+
+        public float GetSingle()
+        {
+            if (TryGetSingle(out float value))
+            {
+                return value;
+            }
+
+            throw new FormatException();
+        }
+
+        public bool TryGetDecimal(out decimal value)
+        {
+            CheckValidInstance();
+
+            return _parent.TryGetValue(_idx, out value);
+        }
+
+        public decimal GetDecimal()
+        {
+            if (TryGetDecimal(out decimal value))
+            {
+                return value;
+            }
+
+            throw new FormatException();
+        }
+
+        internal string GetPropertyName()
+        {
+            CheckValidInstance();
+
+            return _parent.GetNameOfPropertyValue(_idx);
+        }
+
+        public string GetRawText()
+        {
+            CheckValidInstance();
+
+            return _parent.GetRawValueAsString(_idx);
+        }
+
+        internal string GetPropertyRawText()
+        {
+            CheckValidInstance();
+
+            return _parent.GetPropertyRawValueAsString(_idx);
+        }
+
+        public ArrayEnumerator EnumerateArray()
+        {
+            CheckValidInstance();
+
+            JsonTokenType tokenType = TokenType;
+
+            if (tokenType != JsonTokenType.StartArray)
+            {
+                throw ThrowHelper.GetJsonElementWrongTypeException(JsonTokenType.StartArray, tokenType);
+            }
+
+            return new ArrayEnumerator(this);
+        }
+
+        public ObjectEnumerator EnumerateObject()
+        {
+            CheckValidInstance();
+
+            JsonTokenType tokenType = TokenType;
+
+            if (tokenType != JsonTokenType.StartObject)
+            {
+                throw ThrowHelper.GetJsonElementWrongTypeException(JsonTokenType.StartObject, tokenType);
+            }
+
+            return new ObjectEnumerator(this);
+        }
+
+        public override string ToString()
+        {
+            switch (TokenType)
+            {
+                case JsonTokenType.None:
+                case JsonTokenType.Null:
+                    return string.Empty;
+                case JsonTokenType.True:
+                    return bool.TrueString;
+                case JsonTokenType.False:
+                    return bool.FalseString;
+                case JsonTokenType.Number:
+                case JsonTokenType.StartArray:
+                case JsonTokenType.StartObject:
+                {
+                    // null parent should have hit the None case
+                    Debug.Assert(_parent != null);
+                    return _parent.GetRawValueAsString(_idx);
+                }
+                case JsonTokenType.String:
+                    return GetString();
+                case JsonTokenType.Comment:
+                case JsonTokenType.EndArray:
+                case JsonTokenType.EndObject:
+                default:
+                    Debug.Fail($"No handler for {nameof(JsonTokenType)}.{TokenType}");
+                    return string.Empty;
+            }
+        }
+
+        private void CheckValidInstance()
+        {
+            if (_parent == null)
+            {
+                throw new InvalidOperationException();
+            }
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonProperty.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonProperty.cs
new file mode 100644 (file)
index 0000000..ee3a6f8
--- /dev/null
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System.Text.Json
+{
+    public readonly struct JsonProperty
+    {
+        public JsonElement Value { get; }
+
+        internal JsonProperty(JsonElement value)
+        {
+            Value = value;
+        }
+
+        public string Name => Value.GetPropertyName();
+
+        public override string ToString()
+        {
+            return Value.GetPropertyRawText();
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonValueType.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonValueType.cs
new file mode 100644 (file)
index 0000000..6f6dc41
--- /dev/null
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System.Text.Json
+{
+    public enum JsonValueType : byte
+    {
+        Undefined,
+        Object,
+        Array,
+        String,
+        Number,
+        True,
+        False,
+        Null,
+    }
+}
index 8ecdb54..5a4e8f7 100644 (file)
@@ -70,7 +70,47 @@ namespace System.Text.Json
             }
         }
 
-        private static void Unescape(ReadOnlySpan<byte> source, Span<byte> destination, int idx, out int written)
+        internal static int GetUtf8ByteCount(ReadOnlySpan<char> text)
+        {
+#if BUILDING_INBOX_LIBRARY
+            return s_utf8Encoding.GetByteCount(text);
+#else
+            if (text.IsEmpty)
+            {
+                return 0;
+            }
+            unsafe
+            {
+                fixed (char* charPtr = text)
+                {
+                    return s_utf8Encoding.GetByteCount(charPtr, text.Length);
+                }
+            }
+#endif
+        }
+
+        internal static int GetUtf8FromText(ReadOnlySpan<char> text, Span<byte> dest)
+        {
+#if BUILDING_INBOX_LIBRARY
+            return s_utf8Encoding.GetBytes(text, dest);
+#else
+            if (text.IsEmpty)
+            {
+                return 0;
+            }
+
+            unsafe
+            {
+                fixed (char* charPtr = text)
+                fixed (byte* destPtr = dest)
+                {
+                    return s_utf8Encoding.GetBytes(charPtr, text.Length, destPtr, dest.Length);
+                }
+            }
+#endif
+        }
+
+        internal static void Unescape(ReadOnlySpan<byte> source, Span<byte> destination, int idx, out int written)
         {
             Debug.Assert(idx >= 0 && idx < source.Length);
             Debug.Assert(source[idx] == JsonConstants.BackSlash);
index 66b9263..96727ce 100644 (file)
@@ -30,6 +30,28 @@ namespace System.Text.Json
             return (newLines, lastLineFeedIndex);
         }
 
+        internal static JsonValueType ToValueType(this JsonTokenType tokenType)
+        {
+            switch (tokenType)
+            {
+                case JsonTokenType.None:
+                    return JsonValueType.Undefined;
+                case JsonTokenType.StartArray:
+                    return JsonValueType.Array;
+                case JsonTokenType.StartObject:
+                    return JsonValueType.Object;
+                case JsonTokenType.String:
+                case JsonTokenType.Number:
+                case JsonTokenType.True:
+                case JsonTokenType.False:
+                case JsonTokenType.Null:
+                    return (JsonValueType)((byte)tokenType - 3);
+                default:
+                    Debug.Fail($"No mapping for token type {tokenType}");
+                    return JsonValueType.Undefined;
+            }
+        }
+
         // A digit is valid if it is in the range: [0..9]
         // Otherwise, return false.
         public static bool IsDigit(byte nextByte) => (uint)(nextByte - '0') <= '9' - '0';
index 9f6b41c..224440e 100644 (file)
@@ -23,6 +23,7 @@ namespace System.Text.Json
         internal bool _inObject;
         internal bool _isNotPrimitive;
         internal char _numberFormat;
+        internal bool _stringHasEscaping;
         internal JsonTokenType _tokenType;
         internal JsonTokenType _previousTokenType;
         internal JsonReaderOptions _readerOptions;
@@ -71,6 +72,7 @@ namespace System.Text.Json
             _inObject = default;
             _isNotPrimitive = default;
             _numberFormat = default;
+            _stringHasEscaping = default;
             _tokenType = default;
             _previousTokenType = default;
             _readerOptions = options;
index c05c55f..862c8fc 100644 (file)
@@ -35,6 +35,7 @@ namespace System.Text.Json
             _inObject = state._inObject;
             _isNotPrimitive = state._isNotPrimitive;
             _numberFormat = state._numberFormat;
+            _stringHasEscaping = state._stringHasEscaping;
             _tokenType = state._tokenType;
             _previousTokenType = state._previousTokenType;
             _readerOptions = state._readerOptions;
@@ -725,6 +726,7 @@ namespace System.Text.Json
                     _bytePositionInLine += idx + 2; // Add 2 for the start and end quotes.
                     ValueSpan = localBuffer.Slice(0, idx);
                     HasValueSequence = false;
+                    _stringHasEscaping = false;
                     _tokenType = JsonTokenType.String;
                     _consumed += idx + 2;
                     return true;
@@ -782,6 +784,7 @@ namespace System.Text.Json
                         _bytePositionInLine += leftOver + idx + 1;  // Add 1 for the end quote of the string.
                         _totalConsumed += leftOver;
                         _consumed = idx + 1;    // Add 1 for the end quote of the string.
+                        _stringHasEscaping = false;
                         break;
                     }
                     else
@@ -789,6 +792,7 @@ namespace System.Text.Json
                         long prevLineNumber = _lineNumber;
 
                         _bytePositionInLine += idx + 1; // Add 1 for the first quote
+                        _stringHasEscaping = true;
 
                         bool nextCharEscaped = false;
                         bool sawNewLine = false;
@@ -1098,6 +1102,8 @@ namespace System.Text.Json
                 _consumed += idx + 2;
                 ValueSpan = data.Slice(0, idx);
             }
+
+            _stringHasEscaping = true;
             _tokenType = JsonTokenType.String;
             return true;
         }
@@ -1200,7 +1206,7 @@ namespace System.Text.Json
 
             Debug.Assert(nextByte == 'E' || nextByte == 'e');
             i++;
-            _numberFormat = 'e';
+            _numberFormat = JsonConstants.ScientificNotationFormat;
             _bytePositionInLine++;
 
             signResult = ConsumeSignMultiSegment(ref data, ref i);
index e4b622b..fdd585e 100644 (file)
@@ -28,11 +28,14 @@ namespace System.Text.Json
 
             ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
 
-            int idx = span.IndexOf(JsonConstants.BackSlash);
-            if (idx != -1)
+            if (_stringHasEscaping)
             {
+                int idx = span.IndexOf(JsonConstants.BackSlash);
+                Debug.Assert(idx != -1);
                 return JsonReaderHelper.GetUnescapedString(span, idx);
             }
+
+            Debug.Assert(span.IndexOf(JsonConstants.BackSlash) == -1);
             return JsonReaderHelper.TranscodeHelper(span);
         }
 
index af8e053..9ed0cfa 100644 (file)
@@ -30,7 +30,7 @@ namespace System.Text.Json
         private int _maxDepth;
         private bool _inObject;
         private bool _isNotPrimitive;
-        private char _numberFormat;
+        internal char _numberFormat;
         private JsonTokenType _tokenType;
         private JsonTokenType _previousTokenType;
         private JsonReaderOptions _readerOptions;
@@ -38,6 +38,7 @@ namespace System.Text.Json
 
         private long _totalConsumed;
         private bool _isLastSegment;
+        internal bool _stringHasEscaping;
         private readonly bool _isSingleSegment;
 
         private SequencePosition _nextPosition;
@@ -72,6 +73,8 @@ namespace System.Text.Json
         /// </summary>
         public int CurrentDepth => _bitStack.CurrentDepth;
 
+        internal bool IsInArray => !_inObject;
+
         /// <summary>
         /// Gets the type of the last processed JSON token in the UTF-8 encoded JSON text.
         /// </summary>
@@ -132,6 +135,7 @@ namespace System.Text.Json
             _inObject = _inObject,
             _isNotPrimitive = _isNotPrimitive,
             _numberFormat = _numberFormat,
+            _stringHasEscaping = _stringHasEscaping,
             _tokenType = _tokenType,
             _previousTokenType = _previousTokenType,
             _readerOptions = _readerOptions,
@@ -163,6 +167,7 @@ namespace System.Text.Json
             _inObject = state._inObject;
             _isNotPrimitive = state._isNotPrimitive;
             _numberFormat = state._numberFormat;
+            _stringHasEscaping = state._stringHasEscaping;
             _tokenType = state._tokenType;
             _previousTokenType = state._previousTokenType;
             _readerOptions = state._readerOptions;
@@ -771,6 +776,7 @@ namespace System.Text.Json
                 {
                     _bytePositionInLine += idx + 2; // Add 2 for the start and end quotes.
                     ValueSpan = localBuffer.Slice(0, idx);
+                    _stringHasEscaping = false;
                     _tokenType = JsonTokenType.String;
                     _consumed += idx + 2;
                     return true;
@@ -880,6 +886,7 @@ namespace System.Text.Json
         Done:
             _bytePositionInLine++;  // Add 1 for the end quote
             ValueSpan = data.Slice(0, idx);
+            _stringHasEscaping = true;
             _tokenType = JsonTokenType.String;
             _consumed += idx + 2;
             return true;
@@ -988,7 +995,7 @@ namespace System.Text.Json
 
             Debug.Assert(nextByte == 'E' || nextByte == 'e');
             i++;
-            _numberFormat = 'e';
+            _numberFormat = JsonConstants.ScientificNotationFormat;
 
             signResult = ConsumeSign(ref data, ref i);
             if (signResult == ConsumeNumberResult.NeedMoreData)
index 0ad99a6..203c14b 100644 (file)
@@ -189,6 +189,24 @@ namespace System.Text.Json
             return new InvalidOperationException(SR.Format(SR.InvalidCast, tokenType, message));
         }
 
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        internal static InvalidOperationException GetJsonElementWrongTypeException(
+            JsonTokenType expectedType,
+            JsonTokenType actualType)
+        {
+            return new InvalidOperationException(
+                SR.Format(SR.JsonElementHasWrongType, expectedType.ToValueType(), actualType.ToValueType()));
+        }
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        internal static InvalidOperationException GetJsonElementWrongTypeException(
+            string expectedTypeName,
+            JsonTokenType actualType)
+        {
+            return new InvalidOperationException(
+                SR.Format(SR.JsonElementHasWrongType, expectedTypeName, actualType.ToValueType()));
+        }
+
         public static void ThrowJsonReaderException(ref Utf8JsonReader json, ExceptionResource resource, byte nextByte = default, ReadOnlySpan<byte> bytes = default)
         {
             throw GetJsonReaderException(ref json, resource, nextByte, bytes);
diff --git a/src/libraries/System.Text.Json/tests/JsonDocumentTests.cs b/src/libraries/System.Text.Json/tests/JsonDocumentTests.cs
new file mode 100644 (file)
index 0000000..8314f40
--- /dev/null
@@ -0,0 +1,2001 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Buffers;
+using System.Collections;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System.Collections.Generic;
+using System.IO;
+using Xunit;
+using System.Buffers.Text;
+using System.IO.Tests;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace System.Text.Json.Tests
+{
+    public static class JsonDocumentTests
+    {
+        private static readonly Dictionary<TestCaseType, string> s_expectedConcat =
+            new Dictionary<TestCaseType, string>();
+
+        private static readonly Dictionary<TestCaseType, string> s_compactJson =
+            new Dictionary<TestCaseType, string>();
+
+        public static IEnumerable<object[]> ReducedTestCases { get; } =
+            new List<object[]>
+            {
+                new object[] { true, TestCaseType.ProjectLockJson, SR.ProjectLockJson},
+                new object[] { true, TestCaseType.Json40KB, SR.Json40KB},
+                new object[] { false, TestCaseType.DeepTree, SR.DeepTree},
+                new object[] { false, TestCaseType.Json400KB, SR.Json400KB},
+            };
+
+        public static IEnumerable<object[]> TestCases { get; } =
+            new List<object[]>
+            {
+                new object[] { true, TestCaseType.Basic, SR.BasicJson},
+                new object[] { true, TestCaseType.BasicLargeNum, SR.BasicJsonWithLargeNum}, // Json.NET treats numbers starting with 0 as octal (0425 becomes 277)
+                new object[] { true, TestCaseType.BroadTree, SR.BroadTree}, // \r\n behavior is different between Json.NET and System.Text.Json
+                new object[] { true, TestCaseType.DeepTree, SR.DeepTree},
+                new object[] { true, TestCaseType.FullSchema1, SR.FullJsonSchema1},
+                new object[] { true, TestCaseType.HelloWorld, SR.HelloWorld},
+                new object[] { true, TestCaseType.LotsOfNumbers, SR.LotsOfNumbers},
+                new object[] { true, TestCaseType.LotsOfStrings, SR.LotsOfStrings},
+                new object[] { true, TestCaseType.ProjectLockJson, SR.ProjectLockJson},
+                new object[] { true, TestCaseType.Json400B, SR.Json400B},
+                new object[] { true, TestCaseType.Json4KB, SR.Json4KB},
+                new object[] { true, TestCaseType.Json40KB, SR.Json40KB},
+                new object[] { true, TestCaseType.Json400KB, SR.Json400KB},
+
+                new object[] { false, TestCaseType.Basic, SR.BasicJson},
+                new object[] { false, TestCaseType.BasicLargeNum, SR.BasicJsonWithLargeNum}, // Json.NET treats numbers starting with 0 as octal (0425 becomes 277)
+                new object[] { false, TestCaseType.BroadTree, SR.BroadTree}, // \r\n behavior is different between Json.NET and System.Text.Json
+                new object[] { false, TestCaseType.DeepTree, SR.DeepTree},
+                new object[] { false, TestCaseType.FullSchema1, SR.FullJsonSchema1},
+                new object[] { false, TestCaseType.HelloWorld, SR.HelloWorld},
+                new object[] { false, TestCaseType.LotsOfNumbers, SR.LotsOfNumbers},
+                new object[] { false, TestCaseType.LotsOfStrings, SR.LotsOfStrings},
+                new object[] { false, TestCaseType.ProjectLockJson, SR.ProjectLockJson},
+                new object[] { false, TestCaseType.Json400B, SR.Json400B},
+                new object[] { false, TestCaseType.Json4KB, SR.Json4KB},
+                new object[] { false, TestCaseType.Json40KB, SR.Json40KB},
+                new object[] { false, TestCaseType.Json400KB, SR.Json400KB},
+            };
+
+        // TestCaseType is only used to give the json strings a descriptive name within the unit tests.
+        public enum TestCaseType
+        {
+            HelloWorld,
+            Basic,
+            BasicLargeNum,
+            ProjectLockJson,
+            FullSchema1,
+            DeepTree,
+            BroadTree,
+            LotsOfNumbers,
+            LotsOfStrings,
+            Json400B,
+            Json4KB,
+            Json40KB,
+            Json400KB,
+        }
+
+        private static string ReadHelloWorld(JToken obj)
+        {
+            string message = (string)obj["message"];
+            return message;
+        }
+
+        private static string ReadJson400KB(JToken obj)
+        {
+            var sb = new StringBuilder(250000);
+            foreach (JToken token in obj)
+            {
+                sb.Append((string)token["_id"]);
+                sb.Append((int)token["index"]);
+                sb.Append((string)token["guid"]);
+                sb.Append((bool)token["isActive"]);
+                sb.Append((string)token["balance"]);
+                sb.Append((string)token["picture"]);
+                sb.Append((int)token["age"]);
+                sb.Append((string)token["eyeColor"]);
+                sb.Append((string)token["name"]);
+                sb.Append((string)token["gender"]);
+                sb.Append((string)token["company"]);
+                sb.Append((string)token["email"]);
+                sb.Append((string)token["phone"]);
+                sb.Append((string)token["address"]);
+                sb.Append((string)token["about"]);
+                sb.Append((string)token["registered"]);
+                sb.Append((double)token["latitude"]);
+                sb.Append((double)token["longitude"]);
+
+                JToken tags = token["tags"];
+                foreach (JToken tag in tags)
+                {
+                    sb.Append((string)tag);
+                }
+                JToken friends = token["friends"];
+                foreach (JToken friend in friends)
+                {
+                    sb.Append((int)friend["id"]);
+                    sb.Append((string)friend["name"]);
+                }
+                sb.Append((string)token["greeting"]);
+                sb.Append((string)token["favoriteFruit"]);
+
+            }
+            return sb.ToString();
+        }
+
+        private static string ReadHelloWorld(JsonElement obj)
+        {
+            string message = obj.GetProperty("message").GetString();
+            return message;
+        }
+
+        private static string ReadJson400KB(JsonElement obj)
+        {
+            var sb = new StringBuilder(250000);
+
+            foreach (JsonElement element in obj.EnumerateArray())
+            {
+                sb.Append(element.GetProperty("_id").GetString());
+                sb.Append(element.GetProperty("index").GetInt32());
+                sb.Append(element.GetProperty("guid").GetString());
+                sb.Append(element.GetProperty("isActive").GetBoolean());
+                sb.Append(element.GetProperty("balance").GetString());
+                sb.Append(element.GetProperty("picture").GetString());
+                sb.Append(element.GetProperty("age").GetInt32());
+                sb.Append(element.GetProperty("eyeColor").GetString());
+                sb.Append(element.GetProperty("name").GetString());
+                sb.Append(element.GetProperty("gender").GetString());
+                sb.Append(element.GetProperty("company").GetString());
+                sb.Append(element.GetProperty("email").GetString());
+                sb.Append(element.GetProperty("phone").GetString());
+                sb.Append(element.GetProperty("address").GetString());
+                sb.Append(element.GetProperty("about").GetString());
+                sb.Append(element.GetProperty("registered").GetString());
+                sb.Append(element.GetProperty("latitude").GetDouble());
+                sb.Append(element.GetProperty("longitude").GetDouble());
+
+                JsonElement tags = element.GetProperty("tags");
+                for (int j = 0; j < tags.GetArrayLength(); j++)
+                {
+                    sb.Append(tags[j].GetString());
+                }
+                JsonElement friends = element.GetProperty("friends");
+                for (int j = 0; j < friends.GetArrayLength(); j++)
+                {
+                    sb.Append(friends[j].GetProperty("id").GetInt32());
+                    sb.Append(friends[j].GetProperty("name").GetString());
+                }
+                sb.Append(element.GetProperty("greeting").GetString());
+                sb.Append(element.GetProperty("favoriteFruit").GetString());
+            }
+
+            return sb.ToString();
+        }
+
+        [Theory]
+        // The ReadOnlyMemory<bytes> variant is the only one that runs all the input documents.
+        // The rest use a reduced set as because (implementation detail) they ultimately
+        // funnel into the same worker code and the difference between Reduced and Full
+        // is about 0.7 seconds (which adds up).
+        //
+        // If the internals change such that one of these is exercising substantially different
+        // code, then it should switch to the full variation set.
+        [MemberData(nameof(TestCases))]
+        public static void ParseJson_MemoryBytes(bool compactData, TestCaseType type, string jsonString)
+        {
+            ParseJson(
+                compactData,
+                type,
+                jsonString,
+                null,
+                bytes => JsonDocument.Parse(bytes.AsMemory()));
+        }
+
+        [Theory]
+        [MemberData(nameof(ReducedTestCases))]
+        public static void ParseJson_String(bool compactData, TestCaseType type, string jsonString)
+        {
+            ParseJson(
+                compactData,
+                type,
+                jsonString,
+                str => JsonDocument.Parse(str),
+                null);
+        }
+
+        [Theory]
+        [MemberData(nameof(ReducedTestCases))]
+        public static void ParseJson_SeekableStream(bool compactData, TestCaseType type, string jsonString)
+        {
+            ParseJson(
+                compactData,
+                type,
+                jsonString,
+                null,
+                bytes => JsonDocument.Parse(new MemoryStream(bytes)));
+        }
+
+        [Theory]
+        [MemberData(nameof(ReducedTestCases))]
+        public static void ParseJson_SeekableStream_Async(bool compactData, TestCaseType type, string jsonString)
+        {
+            ParseJson(
+                compactData,
+                type,
+                jsonString,
+                null,
+                bytes => JsonDocument.ParseAsync(new MemoryStream(bytes)).GetAwaiter().GetResult());
+        }
+
+        [Theory]
+        [MemberData(nameof(ReducedTestCases))]
+        public static void ParseJson_UnseekableStream(bool compactData, TestCaseType type, string jsonString)
+        {
+            ParseJson(
+                compactData,
+                type,
+                jsonString,
+                null,
+                bytes => JsonDocument.Parse(
+                    new WrappedMemoryStream(canRead: true, canWrite: false, canSeek: false, bytes)));
+        }
+
+        [Theory]
+        [MemberData(nameof(ReducedTestCases))]
+        public static void ParseJson_UnseekableStream_Async(bool compactData, TestCaseType type, string jsonString)
+        {
+            ParseJson(
+                compactData,
+                type,
+                jsonString,
+                null,
+                bytes => JsonDocument.ParseAsync(
+                    new WrappedMemoryStream(canRead: true, canWrite: false, canSeek: false, bytes)).
+                    GetAwaiter().GetResult());
+        }
+
+        [Theory]
+        [MemberData(nameof(ReducedTestCases))]
+        public static void ParseJson_SequenceBytes_Single(bool compactData, TestCaseType type, string jsonString)
+        {
+            ParseJson(
+                compactData,
+                type,
+                jsonString,
+                null,
+                bytes => JsonDocument.Parse(new ReadOnlySequence<byte>(bytes)));
+        }
+
+        [Theory]
+        [MemberData(nameof(ReducedTestCases))]
+        public static void ParseJson_SequenceBytes_Multi(bool compactData, TestCaseType type, string jsonString)
+        {
+            ParseJson(
+                compactData,
+                type,
+                jsonString,
+                null,
+                bytes => JsonDocument.Parse(JsonTestHelper.SegmentInto(bytes, 31)));
+        }
+
+        private static void ParseJson(
+            bool compactData,
+            TestCaseType type,
+            string jsonString,
+            Func<string, JsonDocument> stringDocBuilder,
+            Func<byte[], JsonDocument> bytesDocBuilder)
+        {
+            // One, but not both, must be null.
+            if ((stringDocBuilder == null) == (bytesDocBuilder == null))
+                throw new InvalidOperationException();
+
+            // Remove all formatting/indentation
+            if (compactData)
+            {
+                jsonString = GetCompactJson(type, jsonString);
+            }
+
+            byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+            using (JsonDocument doc = stringDocBuilder?.Invoke(jsonString) ?? bytesDocBuilder?.Invoke(dataUtf8))
+            {
+                JsonElement rootElement = doc.RootElement;
+
+                Func<JToken, string> expectedFunc = null;
+                Func<JsonElement, string> actualFunc = null;
+
+                switch (type)
+                {
+                    case TestCaseType.Json400KB:
+                        expectedFunc = token => ReadJson400KB(token);
+                        actualFunc = element => ReadJson400KB(element);
+                        break;
+                    case TestCaseType.HelloWorld:
+                        expectedFunc = token => ReadHelloWorld(token);
+                        actualFunc = element => ReadHelloWorld(element);
+                        break;
+                }
+
+                if (expectedFunc != null)
+                {
+                    string expectedCustom;
+                    string actualCustom;
+
+                    using (var stream = new MemoryStream(dataUtf8))
+                    using (var streamReader = new StreamReader(stream, Encoding.UTF8, false, 1024, true))
+                    using (JsonTextReader jsonReader = new JsonTextReader(streamReader))
+                    {
+                        JToken jToken = JToken.ReadFrom(jsonReader);
+
+                        expectedCustom = expectedFunc(jToken);
+                        actualCustom = actualFunc(rootElement);
+                    }
+
+                    Assert.Equal(expectedCustom, actualCustom);
+                }
+
+                string actual = doc.PrintJson();
+                string expected = GetExpectedConcat(type, jsonString);
+
+                Assert.Equal(expected, actual);
+
+                Assert.Equal(jsonString, rootElement.GetRawText());
+            }
+        }
+
+        private static string PrintJson(this JsonDocument document, int sizeHint=0)
+        {
+            return PrintJson(document.RootElement, sizeHint);
+        }
+
+        private static string PrintJson(this JsonElement element, int sizeHint=0)
+        {
+            StringBuilder sb = new StringBuilder(sizeHint);
+            DepthFirstAppend(sb, element);
+            return sb.ToString();
+        }
+
+        private static void DepthFirstAppend(StringBuilder buf, JsonElement element)
+        {
+            JsonValueType type = element.Type;
+
+            switch (type)
+            {
+                case JsonValueType.False:
+                case JsonValueType.True:
+                case JsonValueType.String:
+                case JsonValueType.Number:
+                {
+                    buf.Append(element.ToString());
+                    buf.Append(", ");
+                    break;
+                }
+                case JsonValueType.Object:
+                {
+                    foreach (JsonProperty prop in element.EnumerateObject())
+                    {
+                        buf.Append(prop.Name);
+                        buf.Append(", ");
+                        DepthFirstAppend(buf, prop.Value);
+                    }
+
+                    break;
+                }
+                case JsonValueType.Array:
+                {
+                    foreach (JsonElement child in element.EnumerateArray())
+                    {
+                        DepthFirstAppend(buf, child);
+                    }
+
+                    break;
+                }
+            }
+        }
+
+        [Theory]
+        [InlineData("[{\"arrayWithObjects\":[\"text\",14,[],null,false,{},{\"time\":24},[\"1\",\"2\",\"3\"]]}]")]
+        [InlineData("[{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}]")]
+        [InlineData("[{\"a\":{}},{\"a\":{}},{\"a\":{}},{\"a\":{}},{\"a\":{}},{\"a\":{}},{\"a\":{}},{\"a\":{}},{\"a\":{}},{\"a\":{}},{\"a\":{}},{\"a\":{}},{\"a\":{}},{\"a\":{}},{\"a\":{}},{\"a\":{}},{\"a\":{}},{\"a\":{}}]")]
+        [InlineData("{\"a\":\"b\"}")]
+        [InlineData("{}")]
+        [InlineData("[]")]
+        public static void CustomParseJson(string jsonString)
+        {
+            byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+            using (JsonDocument doc = JsonDocument.Parse(dataUtf8, default))
+            {
+                string actual = doc.PrintJson();
+
+                TextReader reader = new StringReader(jsonString);
+                string expected = JsonTestHelper.NewtonsoftReturnStringHelper(reader);
+
+                Assert.Equal(expected, actual);
+            }
+        }
+
+        [Fact]
+        public static void ParseArray()
+        {
+            using (JsonDocument doc = JsonDocument.Parse(SR.SimpleArrayJson))
+            {
+                JsonElement root = doc.RootElement;
+
+                Assert.Equal(2, root.GetArrayLength());
+
+                string phoneNumber = root[0].GetString();
+                int age = root[1].GetInt32();
+
+                Assert.Equal("425-214-3151", phoneNumber);
+                Assert.Equal(25, age);
+
+                Assert.Throws<IndexOutOfRangeException>(() => root[2]);
+            }
+        }
+
+        [Fact]
+        public static void ParseSimpleObject()
+        {
+            using (JsonDocument doc = JsonDocument.Parse(SR.SimpleObjectJson))
+            {
+                JsonElement parsedObject = doc.RootElement;
+
+                int age = parsedObject.GetProperty("age").GetInt32();
+                string ageString = parsedObject.GetProperty("age").ToString();
+                string first = parsedObject.GetProperty("first").GetString();
+                string last = parsedObject.GetProperty("last").GetString();
+                string phoneNumber = parsedObject.GetProperty("phoneNumber").GetString();
+                string street = parsedObject.GetProperty("street").GetString();
+                string city = parsedObject.GetProperty("city").GetString();
+                int zip = parsedObject.GetProperty("zip").GetInt32();
+
+                Assert.True(parsedObject.TryGetProperty("age", out JsonElement age2));
+                Assert.Equal(30, age2.GetInt32());
+
+                Assert.Equal(30, age);
+                Assert.Equal("30", ageString);
+                Assert.Equal("John", first);
+                Assert.Equal("Smith", last);
+                Assert.Equal("425-214-3151", phoneNumber);
+                Assert.Equal("1 Microsoft Way", street);
+                Assert.Equal("Redmond", city);
+                Assert.Equal(98052, zip);
+            }
+        }
+
+        [Fact]
+        public static void ParseNestedJson()
+        {
+            using (JsonDocument doc = JsonDocument.Parse(SR.ParseJson))
+            {
+                JsonElement parsedObject = doc.RootElement;
+
+                Assert.Equal(1, parsedObject.GetArrayLength());
+                JsonElement person = parsedObject[0];
+                double age = person.GetProperty("age").GetDouble();
+                string first = person.GetProperty("first").GetString();
+                string last = person.GetProperty("last").GetString();
+                JsonElement phoneNums = person.GetProperty("phoneNumbers");
+                Assert.Equal(2, phoneNums.GetArrayLength());
+                string phoneNum1 = phoneNums[0].GetString();
+                string phoneNum2 = phoneNums[1].GetString();
+                JsonElement address = person.GetProperty("address");
+                string street = address.GetProperty("street").GetString();
+                string city = address.GetProperty("city").GetString();
+                double zipCode = address.GetProperty("zip").GetDouble();
+
+                Assert.Equal(30, age);
+                Assert.Equal("John", first);
+                Assert.Equal("Smith", last);
+                Assert.Equal("425-000-1212", phoneNum1);
+                Assert.Equal("425-000-1213", phoneNum2);
+                Assert.Equal("1 Microsoft Way", street);
+                Assert.Equal("Redmond", city);
+                Assert.Equal(98052, zipCode);
+
+                Assert.Throws<InvalidOperationException>(() => person.GetArrayLength());
+                Assert.Throws<IndexOutOfRangeException>(() => phoneNums[2]);
+                Assert.Throws<InvalidOperationException>(() => phoneNums.GetProperty("2"));
+                Assert.Throws<KeyNotFoundException>(() => address.GetProperty("2"));
+                Assert.Throws<InvalidOperationException>(() => address.GetProperty("city").GetDouble());
+                Assert.Throws<InvalidOperationException>(() => address.GetProperty("city").GetBoolean());
+                Assert.Throws<InvalidOperationException>(() => address.GetProperty("zip").GetString());
+                Assert.Throws<InvalidOperationException>(() => person.GetProperty("phoneNumbers").GetString());
+                Assert.Throws<InvalidOperationException>(() => person.GetString());
+            }
+        }
+
+        [Fact]
+        public static void ParseBoolean()
+        {
+            using (JsonDocument doc = JsonDocument.Parse("[true,false]"))
+            {
+                JsonElement parsedObject = doc.RootElement;
+                bool first = parsedObject[0].GetBoolean();
+                bool second = parsedObject[1].GetBoolean();
+                Assert.Equal(true, first);
+                Assert.Equal(false, second);
+            }
+        }
+
+        [Fact]
+        public static void JsonArrayToString()
+        {
+            using (JsonDocument doc = JsonDocument.Parse(SR.ParseJson))
+            {
+                JsonElement root = doc.RootElement;
+
+                Assert.Equal(JsonValueType.Array, root.Type);
+                Assert.Equal(SR.ParseJson, root.ToString());
+            }
+        }
+
+        [Fact]
+        public static void JsonObjectToString()
+        {
+            using (JsonDocument doc = JsonDocument.Parse(SR.BasicJson))
+            {
+                JsonElement root = doc.RootElement;
+
+                Assert.Equal(JsonValueType.Object, root.Type);
+                Assert.Equal(SR.BasicJson, root.ToString());
+            }
+        }
+
+        [Fact]
+        public static void MixedArrayIndexing()
+        {
+            // The root object is an array with "complex" children
+            // root[0] is a number (simple single forward)
+            // root[1] is an object which needs to account for the start entry, the children, and end.
+            // root[2] is the target inner array
+            // root[3] is a simple value past two complex values
+            //
+            // Within root[2] the array has only simple values, so it uses a different indexing algorithm.
+            const string json = " [ 6, { \"hi\": \"mom\" }, [ \"425-214-3151\", 25 ], null ] ";
+
+            using (JsonDocument doc = JsonDocument.Parse(json))
+            {
+                JsonElement root = doc.RootElement;
+                JsonElement target = root[2];
+
+                Assert.Equal(2, target.GetArrayLength());
+
+                string phoneNumber = target[0].GetString();
+                int age = target[1].GetInt32();
+
+                Assert.Equal("425-214-3151", phoneNumber);
+                Assert.Equal(25, age);
+                Assert.Equal(JsonValueType.Null, root[3].Type);
+
+                Assert.Throws<IndexOutOfRangeException>(() => root[4]);
+            }
+        }
+
+        [Theory]
+        [InlineData(0)]
+        [InlineData(int.MaxValue)]
+        [InlineData(int.MinValue)]
+        public static void ReadSmallInteger(int value)
+        {
+            double expectedDouble = value;
+            float expectedFloat = value;
+            decimal expectedDecimal = value;
+
+            using (JsonDocument doc = JsonDocument.Parse("    " + value + "  "))
+            {
+                JsonElement root = doc.RootElement;
+
+                Assert.Equal(JsonValueType.Number, root.Type);
+
+                Assert.True(root.TryGetSingle(out float floatVal));
+                Assert.Equal(expectedFloat, floatVal);
+
+                Assert.True(root.TryGetDouble(out double doubleVal));
+                Assert.Equal(expectedDouble, doubleVal);
+
+                Assert.True(root.TryGetDecimal(out decimal decimalVal));
+                Assert.Equal(expectedDecimal, decimalVal);
+
+                Assert.True(root.TryGetInt32(out int intVal));
+                Assert.Equal(value, intVal);
+
+                Assert.True(root.TryGetInt64(out long longVal));
+                Assert.Equal(value, longVal);
+
+                Assert.Equal(expectedFloat, root.GetSingle());
+                Assert.Equal(expectedDouble, root.GetDouble());
+                Assert.Equal(expectedDecimal, root.GetDecimal());
+                Assert.Equal(value, root.GetInt32());
+                Assert.Equal(value, root.GetInt64());
+
+                if (value >= 0)
+                {
+                    uint expectedUInt = (uint)value;
+                    ulong expectedULong = (ulong)value;
+
+                    Assert.True(root.TryGetUInt32(out uint uintVal));
+                    Assert.Equal(expectedUInt, uintVal);
+
+                    Assert.True(root.TryGetUInt64(out ulong ulongVal));
+                    Assert.Equal(expectedULong, ulongVal);
+
+                    Assert.Equal(expectedUInt, root.GetUInt32());
+                    Assert.Equal(expectedULong, root.GetUInt64());
+                }
+                else
+                {
+                    Assert.False(root.TryGetUInt32(out uint uintValue));
+                    Assert.Equal(0U, uintValue);
+
+                    Assert.False(root.TryGetUInt64(out ulong ulongValue));
+                    Assert.Equal(0UL, ulongValue);
+
+                    Assert.Throws<FormatException>(() => root.GetUInt32());
+                    Assert.Throws<FormatException>(() => root.GetUInt64());
+                }
+
+                Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetArrayLength());
+                Assert.Throws<InvalidOperationException>(() => root.EnumerateArray());
+                Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
+                Assert.Throws<InvalidOperationException>(() => root.GetBoolean());
+            }
+        }
+
+        [Theory]
+        [InlineData((long)int.MaxValue + 1)]
+        [InlineData((long)uint.MaxValue)]
+        [InlineData(long.MaxValue)]
+        [InlineData((long)int.MinValue - 1)]
+        [InlineData(long.MinValue)]
+        public static void ReadMediumInteger(long value)
+        {
+            double expectedDouble = value;
+            float expectedFloat = value;
+            decimal expectedDecimal = value;
+
+            using (JsonDocument doc = JsonDocument.Parse("    " + value + "  "))
+            {
+                JsonElement root = doc.RootElement;
+
+                Assert.Equal(JsonValueType.Number, root.Type);
+
+                Assert.True(root.TryGetSingle(out float floatVal));
+                Assert.Equal(expectedFloat, floatVal);
+
+                Assert.True(root.TryGetDouble(out double doubleVal));
+                Assert.Equal(expectedDouble, doubleVal);
+
+                Assert.True(root.TryGetDecimal(out decimal decimalVal));
+                Assert.Equal(expectedDecimal, decimalVal);
+
+                Assert.False(root.TryGetInt32(out int intVal));
+                Assert.Equal(0, intVal);
+
+                Assert.True(root.TryGetInt64(out long longVal));
+                Assert.Equal(value, longVal);
+
+                Assert.Equal(expectedFloat, root.GetSingle());
+                Assert.Equal(expectedDouble, root.GetDouble());
+                Assert.Equal(expectedDecimal, root.GetDecimal());
+                Assert.Throws<FormatException>(() => root.GetInt32());
+                Assert.Equal(value, root.GetInt64());
+
+                if (value >= 0)
+                {
+                    if (value <= uint.MaxValue)
+                    {
+                        uint expectedUInt = (uint)value;
+                        Assert.True(root.TryGetUInt32(out uint uintVal));
+                        Assert.Equal(expectedUInt, uintVal);
+
+                        Assert.Equal(expectedUInt, root.GetUInt64());
+                    }
+                    else
+                    {
+                        Assert.False(root.TryGetUInt32(out uint uintValue));
+                        Assert.Equal(0U, uintValue);
+
+                        Assert.Throws<FormatException>(() => root.GetUInt32());
+                    }
+
+                    ulong expectedULong = (ulong)value;
+                    Assert.True(root.TryGetUInt64(out ulong ulongVal));
+                    Assert.Equal(expectedULong, ulongVal);
+
+                    Assert.Equal(expectedULong, root.GetUInt64());
+                }
+                else
+                {
+                    Assert.False(root.TryGetUInt32(out uint uintValue));
+                    Assert.Equal(0U, uintValue);
+
+                    Assert.False(root.TryGetUInt64(out ulong ulongValue));
+                    Assert.Equal(0UL, ulongValue);
+
+                    Assert.Throws<FormatException>(() => root.GetUInt32());
+                    Assert.Throws<FormatException>(() => root.GetUInt64());
+                }
+
+                Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetArrayLength());
+                Assert.Throws<InvalidOperationException>(() => root.EnumerateArray());
+                Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
+                Assert.Throws<InvalidOperationException>(() => root.GetBoolean());
+            }
+        }
+
+        [Theory]
+        [InlineData((ulong)long.MaxValue + 1)]
+        [InlineData(ulong.MaxValue)]
+        public static void ReadLargeInteger(ulong value)
+        {
+            double expectedDouble = value;
+            float expectedFloat = value;
+            decimal expectedDecimal = value;
+
+            using (JsonDocument doc = JsonDocument.Parse("    " + value + "  "))
+            {
+                JsonElement root = doc.RootElement;
+
+                Assert.Equal(JsonValueType.Number, root.Type);
+
+                Assert.True(root.TryGetSingle(out float floatVal));
+                Assert.Equal(expectedFloat, floatVal);
+
+                Assert.True(root.TryGetDouble(out double doubleVal));
+                Assert.Equal(expectedDouble, doubleVal);
+
+                Assert.True(root.TryGetDecimal(out decimal decimalVal));
+                Assert.Equal(expectedDecimal, decimalVal);
+
+                Assert.False(root.TryGetInt32(out int intVal));
+                Assert.Equal(0, intVal);
+
+                Assert.False(root.TryGetUInt32(out uint uintVal));
+                Assert.Equal(0U, uintVal);
+
+                Assert.False(root.TryGetInt64(out long longVal));
+                Assert.Equal(0L, longVal);
+
+                Assert.Equal(expectedFloat, root.GetSingle());
+                Assert.Equal(expectedDouble, root.GetDouble());
+                Assert.Equal(expectedDecimal, root.GetDecimal());
+                Assert.Throws<FormatException>(() => root.GetInt32());
+                Assert.Throws<FormatException>(() => root.GetUInt32());
+                Assert.Throws<FormatException>(() => root.GetInt64());
+
+                Assert.True(root.TryGetUInt64(out ulong ulongVal));
+                Assert.Equal(value, ulongVal);
+
+                Assert.Equal(value, root.GetUInt64());
+
+                Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetArrayLength());
+                Assert.Throws<InvalidOperationException>(() => root.EnumerateArray());
+                Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
+                Assert.Throws<InvalidOperationException>(() => root.GetBoolean());
+            }
+        }
+
+        [Fact]
+        public static void ReadTooLargeInteger()
+        {
+            float expectedFloat = ulong.MaxValue;
+            double expectedDouble = ulong.MaxValue;
+            decimal expectedDecimal = ulong.MaxValue;
+            expectedDouble *= 10;
+            expectedFloat *= 10;
+            expectedDecimal *= 10;
+
+            using (JsonDocument doc = JsonDocument.Parse("    " + ulong.MaxValue + "0  ", default))
+            {
+                JsonElement root = doc.RootElement;
+
+                Assert.Equal(JsonValueType.Number, root.Type);
+
+                Assert.True(root.TryGetSingle(out float floatVal));
+                Assert.Equal(expectedFloat, floatVal);
+
+                Assert.True(root.TryGetDouble(out double doubleVal));
+                Assert.Equal(expectedDouble, doubleVal);
+
+                Assert.True(root.TryGetDecimal(out decimal decimalVal));
+                Assert.Equal(expectedDecimal, decimalVal);
+
+                Assert.False(root.TryGetInt32(out int intVal));
+                Assert.Equal(0, intVal);
+
+                Assert.False(root.TryGetUInt32(out uint uintVal));
+                Assert.Equal(0U, uintVal);
+
+                Assert.False(root.TryGetInt64(out long longVal));
+                Assert.Equal(0L, longVal);
+
+                Assert.False(root.TryGetUInt64(out ulong ulongVal));
+                Assert.Equal(0UL, ulongVal);
+
+                Assert.Equal(expectedFloat, root.GetSingle());
+                Assert.Equal(expectedDouble, root.GetDouble());
+                Assert.Equal(expectedDecimal, root.GetDecimal());
+                Assert.Throws<FormatException>(() => root.GetInt32());
+                Assert.Throws<FormatException>(() => root.GetUInt32());
+                Assert.Throws<FormatException>(() => root.GetInt64());
+                Assert.Throws<FormatException>(() => root.GetUInt64());
+
+                Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetArrayLength());
+                Assert.Throws<InvalidOperationException>(() => root.EnumerateArray());
+                Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
+                Assert.Throws<InvalidOperationException>(() => root.GetBoolean());
+            }
+        }
+
+        public static IEnumerable<object[]> NonIntegerCases
+        {
+            get
+            {
+                yield return new object[] { "1e+1", 10.0, 10.0f, 10m };
+                yield return new object[] { "1.1e-0", 1.1, 1.1f, 1.1m };
+                yield return new object[] { "3.14159", 3.14159, 3.14159f, 3.14159m };
+                yield return new object[] { "1e-10", 1e-10, 1e-10f, 1e-10m };
+                yield return new object[] { "1234567.15", 1234567.15, 1234567.13f, 1234567.15m };
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(NonIntegerCases))]
+        public static void ReadNonInteger(string str, double expectedDouble, float expectedFloat, decimal expectedDecimal)
+        {
+            using (JsonDocument doc = JsonDocument.Parse("    " + str + "  "))
+            {
+                JsonElement root = doc.RootElement;
+
+                Assert.Equal(JsonValueType.Number, root.Type);
+
+                Assert.True(root.TryGetSingle(out float floatVal));
+                Assert.Equal(expectedFloat, floatVal);
+
+                Assert.True(root.TryGetDouble(out double doubleVal));
+                Assert.Equal(expectedDouble, doubleVal);
+
+                Assert.True(root.TryGetDecimal(out decimal decimalVal));
+                Assert.Equal(expectedDecimal, decimalVal);
+
+                Assert.False(root.TryGetInt32(out int intVal));
+                Assert.Equal(0, intVal);
+
+                Assert.False(root.TryGetInt64(out long longVal));
+                Assert.Equal(0L, longVal);
+
+                Assert.False(root.TryGetUInt64(out ulong ulongVal));
+                Assert.Equal(0UL, ulongVal);
+
+                Assert.Equal(expectedFloat, root.GetSingle());
+                Assert.Equal(expectedDouble, root.GetDouble());
+                Assert.Equal(expectedDecimal, root.GetDecimal());
+                Assert.Throws<FormatException>(() => root.GetInt32());
+                Assert.Throws<FormatException>(() => root.GetInt64());
+                Assert.Throws<FormatException>(() => root.GetUInt64());
+
+                Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetArrayLength());
+                Assert.Throws<InvalidOperationException>(() => root.EnumerateArray());
+                Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
+                Assert.Throws<InvalidOperationException>(() => root.GetBoolean());
+            }
+        }
+
+        [Fact]
+        public static void ReadTooPreciseDouble()
+        {
+            // If https://github.com/dotnet/corefx/issues/33997 gets resolved as the reader throwing,
+            // this test would need to expect FormatException from GetDouble, and false from TryGet.
+            using (JsonDocument doc = JsonDocument.Parse("    1e+100000002"))
+            {
+                JsonElement root = doc.RootElement;
+
+                Assert.Equal(JsonValueType.Number, root.Type);
+
+                if (PlatformDetection.IsFullFramework)
+                {
+                    Assert.False(root.TryGetSingle(out float floatVal));
+                    Assert.Equal(0f, floatVal);
+
+                    Assert.False(root.TryGetDouble(out double doubleVal));
+                    Assert.Equal(0d, doubleVal);
+                }
+                else
+                {
+                    Assert.True(root.TryGetSingle(out float floatVal));
+                    Assert.Equal(float.PositiveInfinity, floatVal);
+
+                    Assert.True(root.TryGetDouble(out double doubleVal));
+                    Assert.Equal(double.PositiveInfinity, doubleVal);
+                }
+
+                Assert.False(root.TryGetDecimal(out decimal decimalVal));
+                Assert.Equal(0m, decimalVal);
+
+                Assert.False(root.TryGetInt32(out int intVal));
+                Assert.Equal(0, intVal);
+
+                Assert.False(root.TryGetInt64(out long longVal));
+                Assert.Equal(0L, longVal);
+
+                Assert.False(root.TryGetUInt64(out ulong ulongVal));
+                Assert.Equal(0UL, ulongVal);
+
+                if (PlatformDetection.IsFullFramework)
+                {
+                    Assert.Throws<FormatException>(() => root.GetSingle());
+                    Assert.Throws<FormatException>(() => root.GetDouble());
+                }
+                else
+                {
+                    Assert.Equal(float.PositiveInfinity, root.GetSingle());
+                    Assert.Equal(double.PositiveInfinity, root.GetDouble());
+                }
+
+                Assert.Throws<FormatException>(() => root.GetDecimal());
+                Assert.Throws<FormatException>(() => root.GetInt32());
+                Assert.Throws<FormatException>(() => root.GetInt64());
+                Assert.Throws<FormatException>(() => root.GetUInt64());
+
+                Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetArrayLength());
+                Assert.Throws<InvalidOperationException>(() => root.EnumerateArray());
+                Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
+                Assert.Throws<InvalidOperationException>(() => root.GetBoolean());
+            }
+        }
+
+        [Fact]
+        public static void ReadArrayWithComments()
+        {
+            // If https://github.com/dotnet/corefx/issues/33997 gets resolved as the reader throwing,
+            // this test would need to expect FormatException from GetDouble, and false from TryGet.
+            JsonReaderOptions options = new JsonReaderOptions
+            {
+                CommentHandling = JsonCommentHandling.Skip,
+            };
+
+            using (JsonDocument doc = JsonDocument.Parse(
+                "[ 0, 1, 2, 3/*.14159*/           , /* 42, 11, hut, hut, hike! */ 4 ]",
+                options))
+            {
+                JsonElement root = doc.RootElement;
+
+                Assert.Equal(JsonValueType.Array, root.Type);
+                Assert.Equal(5, root.GetArrayLength());
+
+                for (int i = root.GetArrayLength() - 1; i >= 0; i--)
+                {
+                    Assert.Equal(i, root[i].GetInt32());
+                }
+
+                int val = 0;
+
+                foreach (JsonElement element in root.EnumerateArray())
+                {
+                    Assert.Equal(val, element.GetInt32());
+                    val++;
+                }
+
+                Assert.Throws<InvalidOperationException>(() => root.GetDouble());
+                Assert.Throws<InvalidOperationException>(() => root.TryGetDouble(out double _));
+                Assert.Throws<InvalidOperationException>(() => root.GetInt32());
+                Assert.Throws<InvalidOperationException>(() => root.TryGetInt32(out int _));
+                Assert.Throws<InvalidOperationException>(() => root.GetInt64());
+                Assert.Throws<InvalidOperationException>(() => root.TryGetInt64(out long _));
+                Assert.Throws<InvalidOperationException>(() => root.GetUInt64());
+                Assert.Throws<InvalidOperationException>(() => root.TryGetUInt64(out ulong _));
+                Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
+                Assert.Throws<InvalidOperationException>(() => root.GetBoolean());
+            }
+        }
+
+        [Fact]
+        public static void CheckUseAfterDispose()
+        {
+            using (JsonDocument doc = JsonDocument.Parse("true", default))
+            {
+                JsonElement root = doc.RootElement;
+                doc.Dispose();
+
+                Assert.Throws<ObjectDisposedException>(() => root.Type);
+                Assert.Throws<ObjectDisposedException>(() => root.GetArrayLength());
+                Assert.Throws<ObjectDisposedException>(() => root.EnumerateArray());
+                Assert.Throws<ObjectDisposedException>(() => root.EnumerateObject());
+                Assert.Throws<ObjectDisposedException>(() => root.GetDouble());
+                Assert.Throws<ObjectDisposedException>(() => root.TryGetDouble(out double _));
+                Assert.Throws<ObjectDisposedException>(() => root.GetInt32());
+                Assert.Throws<ObjectDisposedException>(() => root.TryGetInt32(out int _));
+                Assert.Throws<ObjectDisposedException>(() => root.GetInt64());
+                Assert.Throws<ObjectDisposedException>(() => root.TryGetInt64(out long _));
+                Assert.Throws<ObjectDisposedException>(() => root.GetUInt64());
+                Assert.Throws<ObjectDisposedException>(() => root.TryGetUInt64(out ulong _));
+                Assert.Throws<ObjectDisposedException>(() => root.GetString());
+                Assert.Throws<ObjectDisposedException>(() => root.GetBoolean());
+                Assert.Throws<ObjectDisposedException>(() => root.GetRawText());
+            }
+        }
+
+        [Fact]
+        public static void CheckUseDefault()
+        {
+            JsonElement root = default;
+
+            Assert.Equal(JsonValueType.Undefined, root.Type);
+
+            Assert.Throws<InvalidOperationException>(() => root.GetArrayLength());
+            Assert.Throws<InvalidOperationException>(() => root.EnumerateArray());
+            Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
+            Assert.Throws<InvalidOperationException>(() => root.GetDouble());
+            Assert.Throws<InvalidOperationException>(() => root.TryGetDouble(out double _));
+            Assert.Throws<InvalidOperationException>(() => root.GetInt32());
+            Assert.Throws<InvalidOperationException>(() => root.TryGetInt32(out int _));
+            Assert.Throws<InvalidOperationException>(() => root.GetInt64());
+            Assert.Throws<InvalidOperationException>(() => root.TryGetInt64(out long _));
+            Assert.Throws<InvalidOperationException>(() => root.GetUInt64());
+            Assert.Throws<InvalidOperationException>(() => root.TryGetUInt64(out ulong _));
+            Assert.Throws<InvalidOperationException>(() => root.GetString());
+            Assert.Throws<InvalidOperationException>(() => root.GetBoolean());
+            Assert.Throws<InvalidOperationException>(() => root.GetRawText());
+        }
+
+        [Fact]
+        public static void CheckInvalidString()
+        {
+            Assert.Throws<EncoderFallbackException>(() => JsonDocument.Parse("{ \"unpaired\uDFFE\": true }"));
+        }
+
+        [Theory]
+        [InlineData("\"hello\"    ", "hello")]
+        [InlineData("    null     ", (string)null)]
+        [InlineData("\"\\u0033\\u0031\"", "31")]
+        public static void ReadString(string json, string expectedValue)
+        {
+            using (JsonDocument doc = JsonDocument.Parse(json))
+            {
+                Assert.Equal(expectedValue, doc.RootElement.GetString());
+            }
+        }
+
+        [Fact]
+        public static void GetString_BadUtf8()
+        {
+            // The Arabic ligature Lam-Alef (U+FEFB) (which happens to, as a standalone, mean "no" in English)
+            // is UTF-8 EF BB BB.  So let's leave out a BB and put it in quotes.
+            using (JsonDocument doc = JsonDocument.Parse(new byte[] { 0x22, 0xEF, 0xBB, 0x22 }))
+            {
+                JsonElement root = doc.RootElement;
+
+                Assert.Equal(JsonValueType.String, root.Type);
+                Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetRawText());
+            }
+        }
+
+        [Theory]
+        [InlineData(" { \"hi\": \"there\" }")]
+        [InlineData(" { \n\n\n\n } ")]
+        [InlineData(" { \"outer\": { \"inner\": [ 1, 2, 3 ] }, \"secondOuter\": [ 2, 4, 6, 0, 1 ] }")]
+        public static void TryGetProperty_NoProperty(string json)
+        {
+            using (JsonDocument doc = JsonDocument.Parse(json))
+            {
+                JsonElement root = doc.RootElement;
+
+                const string NotPresent = "Not Present";
+                byte[] notPresentUtf8 = Encoding.UTF8.GetBytes(NotPresent);
+
+                Assert.False(root.TryGetProperty(NotPresent, out _));
+                Assert.False(root.TryGetProperty(NotPresent.AsSpan(), out _));
+                Assert.False(root.TryGetProperty(notPresentUtf8, out _));
+                Assert.False(root.TryGetProperty(new string('z', 512), out _));
+
+                Assert.Throws<KeyNotFoundException>(() => root.GetProperty(NotPresent));
+                Assert.Throws<KeyNotFoundException>(() => root.GetProperty(NotPresent.AsSpan()));
+                Assert.Throws<KeyNotFoundException>(() => root.GetProperty(notPresentUtf8));
+                Assert.Throws<KeyNotFoundException>(() => root.GetProperty(new string('z', 512)));
+            }
+        }
+
+        [Fact]
+        public static void TryGetProperty_CaseSensitive()
+        {
+            const string PascalString = "Needle";
+            const string CamelString = "needle";
+            const string OddString = "nEeDle";
+            const string InverseOddString = "NeEdLE";
+
+            string json = $"{{ \"{PascalString}\": \"no\", \"{CamelString}\": 42, \"{OddString}\": false }}";
+
+            using (JsonDocument doc = JsonDocument.Parse(json))
+            {
+                JsonElement root = doc.RootElement;
+
+                byte[] pascalBytes = Encoding.UTF8.GetBytes(PascalString);
+                byte[] camelBytes = Encoding.UTF8.GetBytes(CamelString);
+                byte[] oddBytes = Encoding.UTF8.GetBytes(OddString);
+                byte[] inverseOddBytes = Encoding.UTF8.GetBytes(InverseOddString);
+
+                void assertPascal(JsonElement elem)
+                {
+                    Assert.Equal(JsonValueType.String, elem.Type);
+                    Assert.Equal("no", elem.GetString());
+                }
+
+                void assertCamel(JsonElement elem)
+                {
+                    Assert.Equal(JsonValueType.Number, elem.Type);
+                    Assert.Equal(42, elem.GetInt32());
+                }
+
+                void assertOdd(JsonElement elem)
+                {
+                    Assert.Equal(JsonValueType.False, elem.Type);
+                    Assert.Equal(false, elem.GetBoolean());
+                }
+
+                Assert.True(root.TryGetProperty(PascalString, out JsonElement pascal));
+                assertPascal(pascal);
+                Assert.True(root.TryGetProperty(PascalString.AsSpan(), out pascal));
+                assertPascal(pascal);
+                Assert.True(root.TryGetProperty(pascalBytes, out pascal));
+                assertPascal(pascal);
+
+                Assert.True(root.TryGetProperty(CamelString, out JsonElement camel));
+                assertCamel(camel);
+                Assert.True(root.TryGetProperty(CamelString.AsSpan(), out camel));
+                assertCamel(camel);
+                Assert.True(root.TryGetProperty(camelBytes, out camel));
+                assertCamel(camel);
+
+                Assert.True(root.TryGetProperty(OddString, out JsonElement odd));
+                assertOdd(odd);
+                Assert.True(root.TryGetProperty(OddString.AsSpan(), out odd));
+                assertOdd(odd);
+                Assert.True(root.TryGetProperty(oddBytes, out odd));
+                assertOdd(odd);
+
+                Assert.False(root.TryGetProperty(InverseOddString, out _));
+                Assert.False(root.TryGetProperty(InverseOddString.AsSpan(), out _));
+                Assert.False(root.TryGetProperty(inverseOddBytes, out _));
+
+                assertPascal(root.GetProperty(PascalString));
+                assertPascal(root.GetProperty(PascalString.AsSpan()));
+                assertPascal(root.GetProperty(pascalBytes));
+
+                assertCamel(root.GetProperty(CamelString));
+                assertCamel(root.GetProperty(CamelString.AsSpan()));
+                assertCamel(root.GetProperty(camelBytes));
+
+                assertOdd(root.GetProperty(OddString));
+                assertOdd(root.GetProperty(OddString.AsSpan()));
+                assertOdd(root.GetProperty(oddBytes));
+
+                Assert.Throws<KeyNotFoundException>(() => root.GetProperty(InverseOddString));
+                Assert.Throws<KeyNotFoundException>(() => root.GetProperty(InverseOddString.AsSpan()));
+                Assert.Throws<KeyNotFoundException>(() => root.GetProperty(inverseOddBytes));
+            }
+        }
+
+        [Theory]
+        [InlineData("")]
+        [InlineData("    ")]
+        [InlineData("1 2")]
+        [InlineData("[ 1")]
+        public static Task CheckUnparsable(string json)
+        {
+            Assert.Throws<JsonReaderException>(() => JsonDocument.Parse(json));
+
+            byte[] utf8 = Encoding.UTF8.GetBytes(json);
+            Assert.Throws<JsonReaderException>(() => JsonDocument.Parse(utf8));
+
+            ReadOnlySequence<byte> singleSeq = new ReadOnlySequence<byte>(utf8);
+            Assert.Throws<JsonReaderException>(() => JsonDocument.Parse(singleSeq));
+
+            ReadOnlySequence<byte> multiSegment = JsonTestHelper.SegmentInto(utf8, 6);
+            Assert.Throws<JsonReaderException>(() => JsonDocument.Parse(multiSegment));
+
+            Stream stream = new MemoryStream(utf8);
+            Assert.Throws<JsonReaderException>(() => JsonDocument.Parse(stream));
+
+            stream.Seek(0, SeekOrigin.Begin);
+            return Assert.ThrowsAsync<JsonReaderException>(() => JsonDocument.ParseAsync(stream));
+        }
+
+        [Fact]
+        public static void CheckParseDepth()
+        {
+            const int OkayCount = 64;
+            string okayJson = new string('[', OkayCount) + "2" + new string(']', OkayCount);
+            int depth = 0;
+
+            using (JsonDocument doc = JsonDocument.Parse(okayJson))
+            {
+                JsonElement root = doc.RootElement;
+                Assert.Equal(JsonValueType.Array, root.Type);
+
+                JsonElement cur = root;
+
+                while (cur.Type == JsonValueType.Array)
+                {
+                    Assert.Equal(1, cur.GetArrayLength());
+                    cur = cur[0];
+                    depth++;
+                }
+
+                Assert.Equal(JsonValueType.Number, cur.Type);
+                Assert.Equal(2, cur.GetInt32());
+                Assert.Equal(OkayCount, depth);
+            }
+
+            string badJson = $"[{okayJson}]";
+
+            Assert.Throws<JsonReaderException>(() => JsonDocument.Parse(badJson));
+        }
+
+        [Fact]
+        public static Task EnableComments()
+        {
+            string json = "3";
+            JsonReaderOptions options = new JsonReaderOptions
+            {
+                CommentHandling = JsonCommentHandling.Allow,
+            };
+
+            AssertExtensions.Throws<ArgumentException>(
+                "readerOptions",
+                () => JsonDocument.Parse(json, options));
+
+            byte[] utf8 = Encoding.UTF8.GetBytes(json);
+            AssertExtensions.Throws<ArgumentException>(
+                "readerOptions",
+                () => JsonDocument.Parse(utf8, options));
+
+            ReadOnlySequence<byte> singleSeq = new ReadOnlySequence<byte>(utf8);
+            AssertExtensions.Throws<ArgumentException>(
+                "readerOptions",
+                () => JsonDocument.Parse(singleSeq, options));
+
+            ReadOnlySequence<byte> multiSegment = JsonTestHelper.SegmentInto(utf8, 6);
+            AssertExtensions.Throws<ArgumentException>(
+                "readerOptions",
+                () => JsonDocument.Parse(multiSegment, options));
+
+            Stream stream = new MemoryStream(utf8);
+            AssertExtensions.Throws<ArgumentException>(
+                "readerOptions",
+                () => JsonDocument.Parse(stream, options));
+
+            stream.Seek(0, SeekOrigin.Begin);
+            return AssertExtensions.ThrowsAsync<ArgumentException>(
+                "readerOptions",
+                () => JsonDocument.ParseAsync(stream, options));
+        }
+
+        [Fact]
+        public static void GetPropertyByNullName()
+        {
+            using (JsonDocument doc = JsonDocument.Parse("{ }"))
+            {
+                AssertExtensions.Throws<ArgumentNullException>(
+                    "propertyName",
+                    () => doc.RootElement.GetProperty((string)null));
+
+                AssertExtensions.Throws<ArgumentNullException>(
+                    "propertyName",
+                    () => doc.RootElement.TryGetProperty((string)null, out _));
+            }
+        }
+
+        [Theory]
+        [InlineData("short")]
+        [InlineData("thisValueIsLongerThan86CharsSoWeDeferTheTranscodingUntilWeFindAViableCandidateAsAPropertyMatch")]
+        public static void GetPropertyFindsLast(string propertyName)
+        {
+            string json = $"{{ \"{propertyName}\": 1, \"{propertyName}\": 2, \"nope\": -1, \"{propertyName}\": 3 }}";
+
+            using (JsonDocument doc = JsonDocument.Parse(json))
+            {
+                JsonElement root = doc.RootElement;
+                byte[] utf8PropertyName = Encoding.UTF8.GetBytes(propertyName);
+
+                Assert.Equal(3, root.GetProperty(propertyName).GetInt32());
+                Assert.Equal(3, root.GetProperty(propertyName.AsSpan()).GetInt32());
+                Assert.Equal(3, root.GetProperty(utf8PropertyName).GetInt32());
+
+                JsonElement matchedProperty;
+                Assert.True(root.TryGetProperty(propertyName, out matchedProperty));
+                Assert.Equal(3, matchedProperty.GetInt32());
+                Assert.True(root.TryGetProperty(propertyName.AsSpan(), out matchedProperty));
+                Assert.Equal(3, matchedProperty.GetInt32());
+                Assert.True(root.TryGetProperty(utf8PropertyName, out matchedProperty));
+                Assert.Equal(3, matchedProperty.GetInt32());
+            }
+        }
+
+        [Theory]
+        [InlineData("short")]
+        [InlineData("thisValueIsLongerThan86CharsSoWeDeferTheTranscodingUntilWeFindAViableCandidateAsAPropertyMatch")]
+        public static void GetPropertyFindsLast_WithEscaping(string propertyName)
+        {
+            string first = $"\\u{(int)propertyName[0]:X4}{propertyName.Substring(1)}";
+            StringBuilder builder = new StringBuilder(propertyName.Length * 6);
+
+            int half = propertyName.Length / 2;
+            builder.Append(propertyName.Substring(0, half));
+
+            for (int i = half; i < propertyName.Length; i++)
+            {
+                builder.AppendFormat("\\u{0:X4}", (int)propertyName[i]);
+            }
+
+            builder.Append('2');
+            string second = builder.ToString();
+            builder.Clear();
+
+            for (int i = 0; i < propertyName.Length; i++)
+            {
+                if ((i & 1) == 0)
+                {
+                    builder.Append(propertyName[i]);
+                }
+                else
+                {
+                    builder.AppendFormat("\\u{0:X4}", (int)propertyName[i]);
+                }
+            }
+
+            builder.Append('3');
+            string third = builder.ToString();
+
+            string pn2 = propertyName + "2";
+            string pn3 = propertyName + "3";
+
+            string json =
+                $"{{ \"{propertyName}\": 0, \"{first}\": 1, \"{pn2}\": 0, \"{second}\": 2, \"{pn3}\": 0, \"nope\": -1, \"{third}\": 3 }}";
+
+            using (JsonDocument doc = JsonDocument.Parse(json))
+            {
+                JsonElement root = doc.RootElement;
+                byte[] utf8PropertyName = Encoding.UTF8.GetBytes(propertyName);
+                byte[] utf8PropertyName2 = Encoding.UTF8.GetBytes(pn2);
+                byte[] utf8PropertyName3 = Encoding.UTF8.GetBytes(pn3);
+
+                Assert.Equal(1, root.GetProperty(propertyName).GetInt32());
+                Assert.Equal(1, root.GetProperty(propertyName.AsSpan()).GetInt32());
+                Assert.Equal(1, root.GetProperty(utf8PropertyName).GetInt32());
+
+                Assert.Equal(2, root.GetProperty(pn2).GetInt32());
+                Assert.Equal(2, root.GetProperty(pn2.AsSpan()).GetInt32());
+                Assert.Equal(2, root.GetProperty(utf8PropertyName2).GetInt32());
+
+                Assert.Equal(3, root.GetProperty(pn3).GetInt32());
+                Assert.Equal(3, root.GetProperty(pn3.AsSpan()).GetInt32());
+                Assert.Equal(3, root.GetProperty(utf8PropertyName3).GetInt32());
+
+                JsonElement matchedProperty;
+                Assert.True(root.TryGetProperty(propertyName, out matchedProperty));
+                Assert.Equal(1, matchedProperty.GetInt32());
+                Assert.True(root.TryGetProperty(propertyName.AsSpan(), out matchedProperty));
+                Assert.Equal(1, matchedProperty.GetInt32());
+                Assert.True(root.TryGetProperty(utf8PropertyName, out matchedProperty));
+                Assert.Equal(1, matchedProperty.GetInt32());
+
+                Assert.True(root.TryGetProperty(pn2, out matchedProperty));
+                Assert.Equal(2, matchedProperty.GetInt32());
+                Assert.True(root.TryGetProperty(pn2.AsSpan(), out matchedProperty));
+                Assert.Equal(2, matchedProperty.GetInt32());
+                Assert.True(root.TryGetProperty(utf8PropertyName2, out matchedProperty));
+                Assert.Equal(2, matchedProperty.GetInt32());
+
+                Assert.True(root.TryGetProperty(pn3, out matchedProperty));
+                Assert.Equal(3, matchedProperty.GetInt32());
+                Assert.True(root.TryGetProperty(pn3.AsSpan(), out matchedProperty));
+                Assert.Equal(3, matchedProperty.GetInt32());
+                Assert.True(root.TryGetProperty(utf8PropertyName3, out matchedProperty));
+                Assert.Equal(3, matchedProperty.GetInt32());
+            }
+        }
+
+        [Fact]
+        public static void GetRawText()
+        {
+            const string json =
+                // Don't let there be a newline before the first embedded quote,
+                // because the index would change across CRLF vs LF compile environments.
+@"{  ""  weird  property  name""
+                  :
+       {
+         ""nested"":
+         [ 1, 2, 3
+,
+4, 5, 6 ],
+        ""also"" : 3
+  },
+  ""number"": 1.02e+4,
+  ""bool"": false,
+  ""n\u0075ll"": null,
+  ""multiLineArray"": 
+
+[
+
+0,
+1,
+2,
+
+    3
+
+],
+  ""string"": 
+
+""Aren't string just the greatest?\r\nNot a terminating quote: \""     \r   \n   \t  \\   ""
+}";
+
+            using (JsonDocument doc = JsonDocument.Parse(json))
+            {
+                JsonElement.ObjectEnumerator enumerator = doc.RootElement.EnumerateObject();
+                Assert.True(enumerator.MoveNext(), "Move to first property");
+                JsonProperty property = enumerator.Current;
+
+                Assert.Equal("  weird  property  name", property.Name);
+                string rawText = property.ToString();
+                int crCount = rawText.Count(c => c == '\r');
+                Assert.Equal(128 + crCount, rawText.Length);
+                Assert.Equal('\"', rawText[0]);
+                Assert.Equal(' ', rawText[1]);
+                Assert.Equal('}', rawText[rawText.Length - 1]);
+                Assert.Equal(json.Substring(json.IndexOf('\"'), rawText.Length), rawText);
+
+                Assert.True(enumerator.MoveNext(), "Move to number property");
+                property = enumerator.Current;
+
+                Assert.Equal("number", property.Name);
+                Assert.Equal("\"number\": 1.02e+4", property.ToString());
+                Assert.Equal(10200.0, property.Value.GetDouble());
+                Assert.Equal("1.02e+4", property.Value.GetRawText());
+
+                Assert.True(enumerator.MoveNext(), "Move to bool property");
+                property = enumerator.Current;
+
+                Assert.Equal("bool", property.Name);
+                Assert.False(property.Value.GetBoolean());
+                Assert.Equal("false", property.Value.GetRawText());
+                Assert.Equal(bool.FalseString, property.Value.ToString());
+
+                Assert.True(enumerator.MoveNext(), "Move to null property");
+                property = enumerator.Current;
+
+                Assert.Equal("null", property.Name);
+                Assert.Equal("null", property.Value.GetRawText());
+                Assert.Equal(string.Empty, property.Value.ToString());
+                Assert.Equal("\"n\\u0075ll\": null", property.ToString());
+
+                Assert.True(enumerator.MoveNext(), "Move to multiLineArray property");
+                property = enumerator.Current;
+
+                Assert.Equal("multiLineArray", property.Name);
+                Assert.Equal(4, property.Value.GetArrayLength());
+                rawText = property.Value.GetRawText();
+                Assert.Equal('[', rawText[0]);
+                Assert.Equal(']', rawText[rawText.Length - 1]);
+                Assert.Contains('3', rawText);
+                Assert.Contains('\n', rawText);
+
+                Assert.True(enumerator.MoveNext(), "Move to string property");
+                property = enumerator.Current;
+
+                Assert.Equal("string", property.Name);
+                rawText = property.Value.GetRawText();
+                Assert.Equal('\"', rawText[0]);
+                Assert.Equal('\"', rawText[rawText.Length - 1]);
+                string strValue = property.Value.GetString();
+                int newlineIdx = strValue.IndexOf('\r');
+                int colonIdx = strValue.IndexOf(':');
+                int escapedQuoteIdx = colonIdx + 2;
+                Assert.Equal(rawText.Substring(1, newlineIdx), strValue.Substring(0, newlineIdx));
+                Assert.Equal('\\', rawText[escapedQuoteIdx + 3]);
+                Assert.Equal('\"', rawText[escapedQuoteIdx + 4]);
+                Assert.Equal('\"', strValue[escapedQuoteIdx]);
+                Assert.Contains("\r", strValue);
+                Assert.Contains(@"\r", rawText);
+                string valueText = rawText;
+                rawText = property.ToString();
+                Assert.Equal('\"', rawText[0]);
+                Assert.Equal('\"', rawText[rawText.Length - 1]);
+                Assert.NotEqual(valueText, rawText);
+                Assert.EndsWith(valueText, rawText);
+
+                Assert.False(enumerator.MoveNext(), "Move past the last property");
+            }
+        }
+
+        [Fact]
+        public static void ArrayEnumeratorIndependentWalk()
+        {
+            using (JsonDocument doc = JsonDocument.Parse("[0, 1, 2, 3, 4, 5]"))
+            {
+                JsonElement root = doc.RootElement;
+                JsonElement.ArrayEnumerator structEnumerable = root.EnumerateArray();
+                IEnumerable<JsonElement> strongBoxedEnumerable = root.EnumerateArray();
+                IEnumerable weakBoxedEnumerable = root.EnumerateArray();
+
+                JsonElement.ArrayEnumerator structEnumerator = structEnumerable.GetEnumerator();
+                IEnumerator<JsonElement> strongBoxedEnumerator = strongBoxedEnumerable.GetEnumerator();
+                IEnumerator weakBoxedEnumerator = weakBoxedEnumerable.GetEnumerator();
+
+                Assert.True(structEnumerator.MoveNext());
+                Assert.True(strongBoxedEnumerator.MoveNext());
+                Assert.True(weakBoxedEnumerator.MoveNext());
+
+                Assert.Equal(0, structEnumerator.Current.GetInt32());
+                Assert.Equal(0, strongBoxedEnumerator.Current.GetInt32());
+                Assert.Equal(0, ((JsonElement)weakBoxedEnumerator.Current).GetInt32());
+
+                Assert.True(structEnumerator.MoveNext());
+                Assert.True(strongBoxedEnumerator.MoveNext());
+                Assert.True(weakBoxedEnumerator.MoveNext());
+
+                Assert.Equal(1, structEnumerator.Current.GetInt32());
+                Assert.Equal(1, strongBoxedEnumerator.Current.GetInt32());
+                Assert.Equal(1, ((JsonElement)weakBoxedEnumerator.Current).GetInt32());
+
+                int test = 0;
+
+                foreach (JsonElement element in structEnumerable)
+                {
+                    Assert.Equal(test, element.GetInt32());
+                    test++;
+                }
+
+                test = 0;
+
+                foreach (JsonElement element in structEnumerable)
+                {
+                    Assert.Equal(test, element.GetInt32());
+                    test++;
+                }
+
+                test = 0;
+
+                foreach (JsonElement element in strongBoxedEnumerable)
+                {
+                    Assert.Equal(test, element.GetInt32());
+                    test++;
+                }
+
+                test = 0;
+
+                foreach (JsonElement element in strongBoxedEnumerable)
+                {
+                    Assert.Equal(test, element.GetInt32());
+                    test++;
+                }
+
+                test = 0;
+
+                foreach (JsonElement element in weakBoxedEnumerable)
+                {
+                    Assert.Equal(test, element.GetInt32());
+                    test++;
+                }
+
+                test = 0;
+
+                foreach (JsonElement element in weakBoxedEnumerable)
+                {
+                    Assert.Equal(test, element.GetInt32());
+                    test++;
+                }
+
+                Assert.True(structEnumerator.MoveNext());
+                Assert.Equal(2, structEnumerator.Current.GetInt32());
+
+                Assert.True(structEnumerator.MoveNext());
+                Assert.Equal(3, structEnumerator.Current.GetInt32());
+
+                Assert.True(strongBoxedEnumerator.MoveNext());
+                Assert.Equal(2, strongBoxedEnumerator.Current.GetInt32());
+
+                Assert.True(structEnumerator.MoveNext());
+                Assert.Equal(4, structEnumerator.Current.GetInt32());
+
+                Assert.True(structEnumerator.MoveNext());
+                Assert.Equal(5, structEnumerator.Current.GetInt32());
+
+                Assert.True(strongBoxedEnumerator.MoveNext());
+                Assert.Equal(3, strongBoxedEnumerator.Current.GetInt32());
+
+                Assert.True(weakBoxedEnumerator.MoveNext());
+                Assert.Equal(2, ((JsonElement)weakBoxedEnumerator.Current).GetInt32());
+
+                Assert.False(structEnumerator.MoveNext());
+
+                Assert.True(strongBoxedEnumerator.MoveNext());
+                Assert.Equal(4, strongBoxedEnumerator.Current.GetInt32());
+
+                Assert.True(strongBoxedEnumerator.MoveNext());
+                Assert.Equal(5, strongBoxedEnumerator.Current.GetInt32());
+
+                Assert.True(weakBoxedEnumerator.MoveNext());
+                Assert.Equal(3, ((JsonElement)weakBoxedEnumerator.Current).GetInt32());
+
+                Assert.True(weakBoxedEnumerator.MoveNext());
+                Assert.Equal(4, ((JsonElement)weakBoxedEnumerator.Current).GetInt32());
+
+                Assert.False(structEnumerator.MoveNext());
+                Assert.False(strongBoxedEnumerator.MoveNext());
+
+                Assert.True(weakBoxedEnumerator.MoveNext());
+                Assert.Equal(5, ((JsonElement)weakBoxedEnumerator.Current).GetInt32());
+
+                Assert.False(weakBoxedEnumerator.MoveNext());
+                Assert.False(structEnumerator.MoveNext());
+                Assert.False(strongBoxedEnumerator.MoveNext());
+                Assert.False(weakBoxedEnumerator.MoveNext());
+            }
+        }
+
+        [Fact]
+        public static void DefaultArrayEnumeratorDoesNotThrow()
+        {
+            JsonElement.ArrayEnumerator enumerable = default;
+            JsonElement.ArrayEnumerator enumerator = enumerable.GetEnumerator();
+            JsonElement.ArrayEnumerator defaultEnumerator = default;
+
+            Assert.Equal(JsonValueType.Undefined, enumerable.Current.Type);
+            Assert.Equal(JsonValueType.Undefined, enumerator.Current.Type);
+
+            Assert.False(enumerable.MoveNext());
+            Assert.False(enumerable.MoveNext());
+            Assert.False(defaultEnumerator.MoveNext());
+        }
+
+        [Fact]
+        public static void ObjectEnumeratorIndependentWalk()
+        {
+            const string json = @"
+{
+  ""name0"": 0,
+  ""name1"": 1,
+  ""name2"": 2,
+  ""name3"": 3,
+  ""name4"": 4,
+  ""name5"": 5
+}";
+            using (JsonDocument doc = JsonDocument.Parse(json))
+            {
+                JsonElement root = doc.RootElement;
+                JsonElement.ObjectEnumerator structEnumerable = root.EnumerateObject();
+                IEnumerable<JsonProperty> strongBoxedEnumerable = root.EnumerateObject();
+                IEnumerable weakBoxedEnumerable = root.EnumerateObject();
+
+                JsonElement.ObjectEnumerator structEnumerator = structEnumerable.GetEnumerator();
+                IEnumerator<JsonProperty> strongBoxedEnumerator = strongBoxedEnumerable.GetEnumerator();
+                IEnumerator weakBoxedEnumerator = weakBoxedEnumerable.GetEnumerator();
+
+                Assert.True(structEnumerator.MoveNext());
+                Assert.True(strongBoxedEnumerator.MoveNext());
+                Assert.True(weakBoxedEnumerator.MoveNext());
+
+                Assert.Equal("name0", structEnumerator.Current.Name);
+                Assert.Equal(0, structEnumerator.Current.Value.GetInt32());
+                Assert.Equal("name0", strongBoxedEnumerator.Current.Name);
+                Assert.Equal(0, strongBoxedEnumerator.Current.Value.GetInt32());
+                Assert.Equal("name0", ((JsonProperty)weakBoxedEnumerator.Current).Name);
+                Assert.Equal(0, ((JsonProperty)weakBoxedEnumerator.Current).Value.GetInt32());
+
+                Assert.True(structEnumerator.MoveNext());
+                Assert.True(strongBoxedEnumerator.MoveNext());
+                Assert.True(weakBoxedEnumerator.MoveNext());
+
+                Assert.Equal(1, structEnumerator.Current.Value.GetInt32());
+                Assert.Equal(1, strongBoxedEnumerator.Current.Value.GetInt32());
+                Assert.Equal(1, ((JsonProperty)weakBoxedEnumerator.Current).Value.GetInt32());
+
+                int test = 0;
+
+                foreach (JsonProperty property in structEnumerable)
+                {
+                    Assert.Equal("name" + test, property.Name);
+                    Assert.Equal(test, property.Value.GetInt32());
+                    test++;
+                }
+
+                test = 0;
+
+                foreach (JsonProperty property in structEnumerable)
+                {
+                    Assert.Equal("name" + test, property.Name);
+                    Assert.Equal(test, property.Value.GetInt32());
+                    test++;
+                }
+
+                test = 0;
+
+                foreach (JsonProperty property in strongBoxedEnumerable)
+                {
+                    Assert.Equal("name" + test, property.Name);
+                    Assert.Equal(test, property.Value.GetInt32());
+                    test++;
+                }
+
+                test = 0;
+
+                foreach (JsonProperty property in strongBoxedEnumerable)
+                {
+                    string propertyName = property.Name;
+                    Assert.Equal("name" + test, property.Name);
+                    Assert.Equal(test, property.Value.GetInt32());
+                    test++;
+
+                    // Subsequent read of the same JsonProperty doesn't allocate a new string
+                    // (if another property is inspected from the same document that guarantee
+                    // doesn't hold).
+                    string propertyName2 = property.Name;
+                    Assert.Same(propertyName, propertyName2);
+                }
+
+                test = 0;
+
+                foreach (JsonProperty property in weakBoxedEnumerable)
+                {
+                    Assert.Equal("name" + test, property.Name);
+                    Assert.Equal(test, property.Value.GetInt32());
+                    test++;
+                }
+
+                test = 0;
+
+                foreach (JsonProperty property in weakBoxedEnumerable)
+                {
+                    Assert.Equal("name" + test, property.Name);
+                    Assert.Equal(test, property.Value.GetInt32());
+                    test++;
+                }
+
+                Assert.True(structEnumerator.MoveNext());
+                Assert.Equal("name2", structEnumerator.Current.Name);
+                Assert.Equal(2, structEnumerator.Current.Value.GetInt32());
+
+                Assert.True(structEnumerator.MoveNext());
+                Assert.Equal("name3", structEnumerator.Current.Name);
+                Assert.Equal(3, structEnumerator.Current.Value.GetInt32());
+
+                Assert.True(strongBoxedEnumerator.MoveNext());
+                Assert.Equal("name2", strongBoxedEnumerator.Current.Name);
+                Assert.Equal(2, strongBoxedEnumerator.Current.Value.GetInt32());
+
+                Assert.True(structEnumerator.MoveNext());
+                Assert.Equal("name4", structEnumerator.Current.Name);
+                Assert.Equal(4, structEnumerator.Current.Value.GetInt32());
+
+                Assert.True(structEnumerator.MoveNext());
+                Assert.Equal("name5", structEnumerator.Current.Name);
+                Assert.Equal(5, structEnumerator.Current.Value.GetInt32());
+
+                Assert.True(strongBoxedEnumerator.MoveNext());
+                Assert.Equal("name3", strongBoxedEnumerator.Current.Name);
+                Assert.Equal(3, strongBoxedEnumerator.Current.Value.GetInt32());
+
+                Assert.True(weakBoxedEnumerator.MoveNext());
+                Assert.Equal("name2", ((JsonProperty)weakBoxedEnumerator.Current).Name);
+                Assert.Equal(2, ((JsonProperty)weakBoxedEnumerator.Current).Value.GetInt32());
+
+                Assert.False(structEnumerator.MoveNext());
+
+                Assert.True(strongBoxedEnumerator.MoveNext());
+                Assert.Equal("name4", strongBoxedEnumerator.Current.Name);
+                Assert.Equal(4, strongBoxedEnumerator.Current.Value.GetInt32());
+
+                Assert.True(strongBoxedEnumerator.MoveNext());
+                Assert.Equal("name5", strongBoxedEnumerator.Current.Name);
+                Assert.Equal(5, strongBoxedEnumerator.Current.Value.GetInt32());
+
+                Assert.True(weakBoxedEnumerator.MoveNext());
+                Assert.Equal("name3", ((JsonProperty)weakBoxedEnumerator.Current).Name);
+                Assert.Equal(3, ((JsonProperty)weakBoxedEnumerator.Current).Value.GetInt32());
+
+                Assert.True(weakBoxedEnumerator.MoveNext());
+                Assert.Equal("name4", ((JsonProperty)weakBoxedEnumerator.Current).Name);
+                Assert.Equal(4, ((JsonProperty)weakBoxedEnumerator.Current).Value.GetInt32());
+
+                Assert.False(structEnumerator.MoveNext());
+                Assert.False(strongBoxedEnumerator.MoveNext());
+
+                Assert.True(weakBoxedEnumerator.MoveNext());
+                Assert.Equal("name5", ((JsonProperty)weakBoxedEnumerator.Current).Name);
+                Assert.Equal(5, ((JsonProperty)weakBoxedEnumerator.Current).Value.GetInt32());
+
+                Assert.False(weakBoxedEnumerator.MoveNext());
+                Assert.False(structEnumerator.MoveNext());
+                Assert.False(strongBoxedEnumerator.MoveNext());
+                Assert.False(weakBoxedEnumerator.MoveNext());
+            }
+        }
+
+        [Fact]
+        public static void DefaultObjectEnumeratorDoesNotThrow()
+        {
+            JsonElement.ObjectEnumerator enumerable = default;
+            JsonElement.ObjectEnumerator enumerator = enumerable.GetEnumerator();
+            JsonElement.ObjectEnumerator defaultEnumerator = default;
+
+            Assert.Equal(JsonValueType.Undefined, enumerable.Current.Value.Type);
+            Assert.Equal(JsonValueType.Undefined, enumerator.Current.Value.Type);
+
+            Assert.Throws<InvalidOperationException>(() => enumerable.Current.Name);
+            Assert.Throws<InvalidOperationException>(() => enumerator.Current.Name);
+
+            Assert.False(enumerable.MoveNext());
+            Assert.False(enumerable.MoveNext());
+            Assert.False(defaultEnumerator.MoveNext());
+        }
+
+        [Fact]
+        public static void ReadNestedObject()
+        {
+            const string json = @"
+{
+  ""first"":
+  {
+    ""true"": true,
+    ""false"": false,
+    ""null"": null,
+    ""int"": 3,
+    ""nearlyPi"": 3.14159,
+    ""text"": ""This is some text that does not end... <EOT>""
+  },
+  ""second"":
+  {
+    ""blub"": { ""bool"": true },
+    ""glub"": { ""bool"": false }
+  }
+}";
+            using (JsonDocument doc = JsonDocument.Parse(json))
+            {
+                JsonElement root = doc.RootElement;
+                Assert.Equal(JsonValueType.Object, root.Type);
+
+                Assert.True(root.GetProperty("first").GetProperty("true").GetBoolean());
+                Assert.False(root.GetProperty("first").GetProperty("false").GetBoolean());
+                Assert.Equal(JsonValueType.Null, root.GetProperty("first").GetProperty("null").Type);
+                Assert.Equal(3, root.GetProperty("first").GetProperty("int").GetInt32());
+                Assert.Equal(3.14159f, root.GetProperty("first").GetProperty("nearlyPi").GetSingle());
+                Assert.Equal("This is some text that does not end... <EOT>", root.GetProperty("first").GetProperty("text").GetString());
+
+                Assert.True(root.GetProperty("second").GetProperty("blub").GetProperty("bool").GetBoolean());
+                Assert.False(root.GetProperty("second").GetProperty("glub").GetProperty("bool").GetBoolean());
+            }
+        }
+
+        [Fact]
+        public static void ParseNull()
+        {
+            Assert.Throws<ArgumentNullException>(
+                "json",
+                () => JsonDocument.Parse((string)null));
+
+            AssertExtensions.Throws<ArgumentNullException>(
+                "utf8Json",
+                () => JsonDocument.Parse((Stream)null));
+
+            // This synchronously throws the ArgumentNullException
+            AssertExtensions.Throws<ArgumentNullException>(
+                "utf8Json",
+                () => { JsonDocument.ParseAsync(null); });
+        }
+
+        [Fact]
+        public static void EnsureResizeSucceeds()
+        {
+            // This test increases coverage, so it's based on a lot of implementation detail,
+            // to ensure that the otherwise untested blocks produce the right functional behavior.
+            //
+            // The initial database size is just over the number of bytes of UTF-8 data in the payload,
+            // capped at 2^20 (unless the payload exceeds 2^22).
+            //
+            // Regrowth happens if the rented array (which may be bigger than we asked for) is not sufficient,
+            // meaning tokens (on average) occur more often than every 12 bytes.
+            //
+            // The array pool (for bytes) returns power-of-two sizes.
+            //
+            // Conclusion: A resize will happen if a payload of 1MB+epsilon has tokens more often than every 12 bytes.
+            //
+            // Integer numbers as strings, padded to 4 with no whitespace in series in an array: 7x + 1.
+            //  That would take 149797 integers.
+            //
+            // Padded to 5 (8x + 1) => 131072 integers.
+            // Padded to 6 (9x + 1) => 116509 integers.
+            //
+            // At pad-to-6 tokens occur every 9 bytes, and we can represent values without repeat.
+
+            const int NumberOfNumbers = (1024 * 1024 / 9) + 1;
+            const int NumberOfBytes = 9 * NumberOfNumbers + 1;
+
+            byte[] utf8Json = new byte[NumberOfBytes];
+            utf8Json.AsSpan().Fill((byte)'"');
+            utf8Json[0] = (byte)'[';
+
+            Span<byte> valuesSpan = utf8Json.AsSpan(1);
+            StandardFormat format = StandardFormat.Parse("D6");
+
+            for (int i = 0; i < NumberOfNumbers; i++)
+            {
+                // Just inside the quote
+                Span<byte> curDest = valuesSpan.Slice(9 * i + 1);
+
+                if (!Utf8Formatter.TryFormat(i, curDest, out int bytesWritten, format) || bytesWritten != 6)
+                {
+                    throw new InvalidOperationException("" + i);
+                }
+
+                curDest[7] = (byte)',';
+            }
+
+            // Replace last comma with ]
+            utf8Json[NumberOfBytes - 1] = (byte)']';
+
+            using (JsonDocument doc = JsonDocument.Parse(utf8Json))
+            {
+                JsonElement root = doc.RootElement;
+                int count = root.GetArrayLength();
+
+                for (int i = 0; i < count; i++)
+                {
+                    Assert.Equal(i, int.Parse(root[i].GetString()));
+                }
+            }
+        }
+
+        private static string GetExpectedConcat(TestCaseType testCaseType, string jsonString)
+        {
+            if (s_expectedConcat.TryGetValue(testCaseType, out string existing))
+            {
+                return existing;
+            }
+
+            TextReader reader = new StringReader(jsonString);
+            return s_expectedConcat[testCaseType] = JsonTestHelper.NewtonsoftReturnStringHelper(reader);
+        }
+
+        private static string GetCompactJson(TestCaseType testCaseType, string jsonString)
+        {
+            if (s_compactJson.TryGetValue(testCaseType, out string existing))
+            {
+                return existing;
+            }
+
+            using (JsonTextReader jsonReader = new JsonTextReader(new StringReader(jsonString)))
+            {
+                jsonReader.FloatParseHandling = FloatParseHandling.Decimal;
+                JToken jtoken = JToken.ReadFrom(jsonReader);
+                var stringWriter = new StringWriter();
+
+                using (JsonTextWriter jsonWriter = new JsonTextWriter(stringWriter))
+                {
+                    jtoken.WriteTo(jsonWriter);
+                    existing = stringWriter.ToString();
+                }
+            }
+
+            return s_compactJson[testCaseType] = existing;
+        }
+    }
+}
index c20a6aa..26e1c0d 100644 (file)
@@ -157,6 +157,40 @@ namespace System.Text.Json.Tests
             return BufferFactory.Create(buffers);
         }
 
+        internal static ReadOnlySequence<byte> SegmentInto(ReadOnlyMemory<byte> data, int segmentCount)
+        {
+            if (segmentCount < 2)
+                throw new ArgumentOutOfRangeException(nameof(segmentCount));
+
+            int perSegment = data.Length / segmentCount;
+            BufferSegment<byte> first;
+
+            if (perSegment == 0 && data.Length > 0)
+            {
+                first = new BufferSegment<byte>(data.Slice(0, 1));
+                data = data.Slice(1);
+            }
+            else
+            {
+                first = new BufferSegment<byte>(data.Slice(0, perSegment));
+                data = data.Slice(perSegment);
+            }
+
+            BufferSegment<byte> last = first;
+            segmentCount--;
+
+            while (segmentCount > 1)
+            {
+                perSegment = data.Length / segmentCount;
+                last = last.Append(data.Slice(0, perSegment));
+                data = data.Slice(perSegment);
+                segmentCount--;
+            }
+
+            last = last.Append(data);
+            return new ReadOnlySequence<byte>(first, 0, last, data.Length);
+        }
+
         public static object ReturnObjectHelper(byte[] data, JsonCommentHandling commentHandling = JsonCommentHandling.Disallow)
         {
             var state = new JsonReaderState(options: new JsonReaderOptions { CommentHandling = commentHandling });
index c5ad70b..ba5a435 100644 (file)
@@ -4,12 +4,16 @@
     <Configurations>netcoreapp-Debug;netcoreapp-Release;uap-Windows_NT-Debug;uap-Windows_NT-Release;netstandard-Debug;netstandard-Release</Configurations>
   </PropertyGroup>
   <ItemGroup>
+    <Compile Include="$(CommonTestPath)\System\IO\WrappedMemoryStream.cs">
+      <Link>CommonTest\System\IO\WrappedMemoryStream.cs</Link>
+    </Compile>
     <Compile Include="ArrayBufferWriter.cs" />
     <Compile Include="BitStackTests.cs" />
     <Compile Include="BufferFactory.cs" />
     <Compile Include="BufferSegment.cs" />
     <Compile Include="FixedSizedBufferWriter.cs" />
     <Compile Include="InvalidBufferWriter.cs" />
+    <Compile Include="JsonDocumentTests.cs" />
     <Compile Include="JsonNumberTestData.cs" />
     <Compile Include="JsonReaderStateTests.cs" />
     <Compile Include="JsonTestHelper.cs" />
@@ -27,4 +31,4 @@
   <ItemGroup>
     <ReferenceFromRuntime Include="Newtonsoft.Json" />
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
index f6c8976..baa4e44 100644 (file)
     "Description": "Provides a fast, non-cached, forward-only way to read UTF-8 encoded JavaScript Object Notation (JSON) text.",
     "CommonTypes": [
       "System.Text.Json.Utf8JsonReader",
-      "System.Text.Json.Utf8JsonWriter"
+      "System.Text.Json.Utf8JsonWriter",
+      "System.Text.Json.JsonDocument"
     ]
   },
   {