diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index d1394f939f1a1..a75a8bb9dd264 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -66,6 +66,7 @@ public readonly partial struct JsonElement public System.Text.Json.JsonElement GetProperty(System.ReadOnlySpan utf8PropertyName) { throw null; } public System.Text.Json.JsonElement GetProperty(System.ReadOnlySpan propertyName) { throw null; } public System.Text.Json.JsonElement GetProperty(string propertyName) { throw null; } + public int GetPropertyCount() { throw null; } public string GetRawText() { throw null; } [System.CLSCompliantAttribute(false)] public sbyte GetSByte() { throw null; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.MetadataDb.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.MetadataDb.cs index a53df982c12eb..dca61c889c89d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.MetadataDb.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.MetadataDb.cs @@ -68,7 +68,7 @@ public sealed partial class JsonDocument // * 31 bits for token offset // * Second int // * Top bit is unassigned / always clear - // * 31 bits for the token length (always 1, effectively unassigned) + // * 31 bits for the number of properties in this object // * Third int // * 4 bits JsonTokenType // * 28 bits for the number of rows until the next value (never 0) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs index ccf524fc40f3a..7b3ffa246a90c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs @@ -950,7 +950,7 @@ private static void Parse( ref StackRowStack stack) { bool inArray = false; - int arrayItemsCount = 0; + int arrayItemsOrPropertyCount = 0; int numberOfRowsForMembers = 0; int numberOfRowsForValues = 0; @@ -972,13 +972,14 @@ private static void Parse( { if (inArray) { - arrayItemsCount++; + arrayItemsOrPropertyCount++; } numberOfRowsForValues++; database.Append(tokenType, tokenStart, DbRow.UnknownSize); - var row = new StackRow(numberOfRowsForMembers + 1); + var row = new StackRow(arrayItemsOrPropertyCount, numberOfRowsForMembers + 1); stack.Push(row); + arrayItemsOrPropertyCount = 0; numberOfRowsForMembers = 0; } else if (tokenType == JsonTokenType.EndObject) @@ -987,7 +988,7 @@ private static void Parse( numberOfRowsForValues++; numberOfRowsForMembers++; - database.SetLength(rowIndex, numberOfRowsForMembers); + database.SetLength(rowIndex, arrayItemsOrPropertyCount); int newRowIndex = database.Length; database.Append(tokenType, tokenStart, reader.ValueSpan.Length); @@ -995,20 +996,21 @@ private static void Parse( database.SetNumberOfRows(newRowIndex, numberOfRowsForMembers); StackRow row = stack.Pop(); - numberOfRowsForMembers += row.SizeOrLength; + arrayItemsOrPropertyCount = row.SizeOrLength; + numberOfRowsForMembers += row.NumberOfRows; } else if (tokenType == JsonTokenType.StartArray) { if (inArray) { - arrayItemsCount++; + arrayItemsOrPropertyCount++; } numberOfRowsForMembers++; database.Append(tokenType, tokenStart, DbRow.UnknownSize); - var row = new StackRow(arrayItemsCount, numberOfRowsForValues + 1); + var row = new StackRow(arrayItemsOrPropertyCount, numberOfRowsForValues + 1); stack.Push(row); - arrayItemsCount = 0; + arrayItemsOrPropertyCount = 0; numberOfRowsForValues = 0; } else if (tokenType == JsonTokenType.EndArray) @@ -1017,7 +1019,7 @@ private static void Parse( numberOfRowsForValues++; numberOfRowsForMembers++; - database.SetLength(rowIndex, arrayItemsCount); + database.SetLength(rowIndex, arrayItemsOrPropertyCount); database.SetNumberOfRows(rowIndex, numberOfRowsForValues); // If the array item count is (e.g.) 12 and the number of rows is (e.g.) 13 @@ -1030,7 +1032,7 @@ private static void Parse( // 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) + if (arrayItemsOrPropertyCount + 1 != numberOfRowsForValues) { database.SetHasComplexChildren(rowIndex); } @@ -1040,13 +1042,14 @@ private static void Parse( database.SetNumberOfRows(newRowIndex, numberOfRowsForValues); StackRow row = stack.Pop(); - arrayItemsCount = row.SizeOrLength; + arrayItemsOrPropertyCount = row.SizeOrLength; numberOfRowsForValues += row.NumberOfRows; } else if (tokenType == JsonTokenType.PropertyName) { numberOfRowsForValues++; numberOfRowsForMembers++; + arrayItemsOrPropertyCount++; // Adding 1 to skip the start quote will never overflow Debug.Assert(tokenStart < int.MaxValue); @@ -1068,7 +1071,7 @@ private static void Parse( if (inArray) { - arrayItemsCount++; + arrayItemsOrPropertyCount++; } if (tokenType == JsonTokenType.String) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs index da72d147abd35..e71c9ae3aa9fe 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs @@ -96,7 +96,7 @@ public int GetArrayLength() /// /// The parent has been disposed. /// - internal int GetPropertyCount() + public int GetPropertyCount() { CheckValidInstance(); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDocumentTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDocumentTests.cs index 518ab0f46e3cf..8be93677470ec 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDocumentTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDocumentTests.cs @@ -681,6 +681,7 @@ public static void ParseSimpleObject() string city = parsedObject.GetProperty("city").GetString(); int zip = parsedObject.GetProperty("zip").GetInt32(); + Assert.Equal(7, parsedObject.GetPropertyCount()); Assert.True(parsedObject.TryGetProperty("age", out JsonElement age2)); Assert.Equal(30, age2.GetInt32()); @@ -704,6 +705,7 @@ public static void ParseNestedJson() Assert.Equal(1, parsedObject.GetArrayLength()); JsonElement person = parsedObject[0]; + Assert.Equal(5, person.GetPropertyCount()); double age = person.GetProperty("age").GetDouble(); string first = person.GetProperty("first").GetString(); string last = person.GetProperty("last").GetString(); @@ -902,6 +904,7 @@ public static void ReadNumber_1Byte(sbyte value) Assert.Throws(() => root.GetDateTimeOffset()); Assert.Throws(() => root.GetGuid()); Assert.Throws(() => root.GetArrayLength()); + Assert.Throws(() => root.GetPropertyCount()); Assert.Throws(() => root.EnumerateArray()); Assert.Throws(() => root.EnumerateObject()); Assert.Throws(() => root.GetBoolean()); @@ -999,6 +1002,7 @@ public static void ReadNumber_2Bytes(short value) Assert.Throws(() => root.GetDateTimeOffset()); Assert.Throws(() => root.GetGuid()); Assert.Throws(() => root.GetArrayLength()); + Assert.Throws(() => root.GetPropertyCount()); Assert.Throws(() => root.EnumerateArray()); Assert.Throws(() => root.EnumerateObject()); Assert.Throws(() => root.GetBoolean()); @@ -1091,6 +1095,7 @@ public static void ReadSmallInteger(int value) Assert.Throws(() => root.GetDateTimeOffset()); Assert.Throws(() => root.GetGuid()); Assert.Throws(() => root.GetArrayLength()); + Assert.Throws(() => root.GetPropertyCount()); Assert.Throws(() => root.EnumerateArray()); Assert.Throws(() => root.EnumerateObject()); Assert.Throws(() => root.GetBoolean()); @@ -1194,6 +1199,7 @@ public static void ReadMediumInteger(long value) Assert.Throws(() => root.GetDateTimeOffset()); Assert.Throws(() => root.GetGuid()); Assert.Throws(() => root.GetArrayLength()); + Assert.Throws(() => root.GetPropertyCount()); Assert.Throws(() => root.EnumerateArray()); Assert.Throws(() => root.EnumerateObject()); Assert.Throws(() => root.GetBoolean()); @@ -1267,6 +1273,7 @@ public static void ReadLargeInteger(ulong value) Assert.Throws(() => root.GetDateTimeOffset()); Assert.Throws(() => root.GetGuid()); Assert.Throws(() => root.GetArrayLength()); + Assert.Throws(() => root.GetPropertyCount()); Assert.Throws(() => root.EnumerateArray()); Assert.Throws(() => root.EnumerateObject()); Assert.Throws(() => root.GetBoolean()); @@ -1340,6 +1347,7 @@ public static void ReadTooLargeInteger() Assert.Throws(() => root.GetDateTimeOffset()); Assert.Throws(() => root.GetGuid()); Assert.Throws(() => root.GetArrayLength()); + Assert.Throws(() => root.GetPropertyCount()); Assert.Throws(() => root.EnumerateArray()); Assert.Throws(() => root.EnumerateObject()); Assert.Throws(() => root.GetBoolean()); @@ -1379,6 +1387,7 @@ public static void ReadDateTimeAndDateTimeOffset(string jsonString, string expec Assert.Throws(() => root.GetInt64()); Assert.Throws(() => root.GetUInt64()); Assert.Throws(() => root.GetArrayLength()); + Assert.Throws(() => root.GetPropertyCount()); Assert.Throws(() => root.EnumerateArray()); Assert.Throws(() => root.EnumerateObject()); Assert.Throws(() => root.GetBoolean()); @@ -1462,6 +1471,7 @@ public static void ReadGuid(string jsonString, string expectedStr) Assert.Throws(() => root.GetInt64()); Assert.Throws(() => root.GetUInt64()); Assert.Throws(() => root.GetArrayLength()); + Assert.Throws(() => root.GetPropertyCount()); Assert.Throws(() => root.EnumerateArray()); Assert.Throws(() => root.EnumerateObject()); Assert.Throws(() => root.GetBoolean()); @@ -1564,6 +1574,7 @@ public static void ReadNonInteger(string str, double expectedDouble, float expec Assert.Throws(() => root.GetDateTimeOffset()); Assert.Throws(() => root.GetGuid()); Assert.Throws(() => root.GetArrayLength()); + Assert.Throws(() => root.GetPropertyCount()); Assert.Throws(() => root.EnumerateArray()); Assert.Throws(() => root.EnumerateObject()); Assert.Throws(() => root.GetBoolean()); @@ -1654,6 +1665,7 @@ public static void ReadTooPreciseDouble() Assert.Throws(() => root.GetDateTimeOffset()); Assert.Throws(() => root.GetGuid()); Assert.Throws(() => root.GetArrayLength()); + Assert.Throws(() => root.GetPropertyCount()); Assert.Throws(() => root.EnumerateArray()); Assert.Throws(() => root.EnumerateObject()); Assert.Throws(() => root.GetBoolean()); @@ -1735,6 +1747,7 @@ public static void CheckUseAfterDispose() Assert.Throws(() => root.ValueKind); Assert.Throws(() => root.GetArrayLength()); + Assert.Throws(() => root.GetPropertyCount()); Assert.Throws(() => root.EnumerateArray()); Assert.Throws(() => root.EnumerateObject()); Assert.Throws(() => root.GetDouble()); @@ -1793,6 +1806,7 @@ public static void CheckUseDefault() Assert.Equal(JsonValueKind.Undefined, root.ValueKind); Assert.Throws(() => root.GetArrayLength()); + Assert.Throws(() => root.GetPropertyCount()); Assert.Throws(() => root.EnumerateArray()); Assert.Throws(() => root.EnumerateObject()); Assert.Throws(() => root.GetDouble()); @@ -2442,6 +2456,7 @@ public static void GetRawText() using (JsonDocument doc = JsonDocument.Parse(json)) { + Assert.Equal(6, doc.RootElement.GetPropertyCount()); JsonElement.ObjectEnumerator enumerator = doc.RootElement.EnumerateObject(); Assert.True(enumerator.MoveNext(), "Move to first property"); JsonProperty property = enumerator.Current; @@ -2449,6 +2464,7 @@ public static void GetRawText() Assert.Equal(" weird property name", property.Name); string rawText = property.ToString(); int crCount = rawText.Count(c => c == '\r'); + Assert.Equal(2, property.Value.GetPropertyCount()); Assert.Equal(128 + crCount, rawText.Length); Assert.Equal('\"', rawText[0]); Assert.Equal(' ', rawText[1]); @@ -3437,6 +3453,46 @@ public static void ParseValue_AllowMultipleValues_TrailingContent() JsonTestHelper.AssertThrows(ref reader, (ref Utf8JsonReader reader) => reader.Read()); } + [Theory] + [InlineData("""{ "foo" : [1], "test": false, "bar" : { "nested": 3 } }""", 3)] + [InlineData("""{ "foo" : [1,2,3,4] }""", 1)] + [InlineData("""{}""", 0)] + [InlineData("""{ "foo" : {"nested:" : {"nested": 1, "bla": [1, 2, {"bla": 3}] } }, "test": true, "foo2" : {"nested:" : {"nested": 1, "bla": [1, 2, {"bla": 3}] } }}""", 3)] + public static void TestGetPropertyCount(string json, int expectedCount) + { + JsonElement element = JsonSerializer.Deserialize(json); + Assert.Equal(expectedCount, element.GetPropertyCount()); + } + + [Fact] + public static void VerifyGetPropertyCountAndArrayLengthUsingEnumerateMethods() + { + using (JsonDocument doc = JsonDocument.Parse(SR.ProjectLockJson)) + { + CheckPropertyCountAndArrayLengthAgainstEnumerateMethods(doc.RootElement); + } + + void CheckPropertyCountAndArrayLengthAgainstEnumerateMethods(JsonElement elem) + { + if (elem.ValueKind == JsonValueKind.Object) + { + Assert.Equal(elem.EnumerateObject().Count(), elem.GetPropertyCount()); + foreach (JsonProperty prop in elem.EnumerateObject()) + { + CheckPropertyCountAndArrayLengthAgainstEnumerateMethods(prop.Value); + } + } + else if (elem.ValueKind == JsonValueKind.Array) + { + Assert.Equal(elem.EnumerateArray().Count(), elem.GetArrayLength()); + foreach (JsonElement item in elem.EnumerateArray()) + { + CheckPropertyCountAndArrayLengthAgainstEnumerateMethods(item); + } + } + } + } + [Fact] public static void EnsureResizeSucceeds() { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.ReadTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.ReadTests.cs index f689cf296ead1..e296ee97c8d06 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.ReadTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.ReadTests.cs @@ -220,11 +220,14 @@ public async Task TestBOMWithShortAndLongBuffers(Stream stream, int count, int e void VerifyElement(int index) { Assert.Equal(JsonValueKind.Object, value[index].GetProperty("Test").ValueKind); + Assert.Equal(0, value[index].GetProperty("Test").GetPropertyCount()); Assert.False(value[index].GetProperty("Test").EnumerateObject().MoveNext()); Assert.Equal(JsonValueKind.Array, value[index].GetProperty("Test2").ValueKind); Assert.Equal(0, value[index].GetProperty("Test2").GetArrayLength()); Assert.Equal(JsonValueKind.Object, value[index].GetProperty("Test3").ValueKind); Assert.Equal(JsonValueKind.Object, value[index].GetProperty("Test3").GetProperty("Value").ValueKind); + Assert.Equal(1, value[index].GetProperty("Test3").GetPropertyCount()); + Assert.Equal(0, value[index].GetProperty("Test3").GetProperty("Value").GetPropertyCount()); Assert.False(value[index].GetProperty("Test3").GetProperty("Value").EnumerateObject().MoveNext()); Assert.Equal(0, value[index].GetProperty("PersonType").GetInt32()); Assert.Equal(2, value[index].GetProperty("Id").GetInt32());