diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index bf54c84e5433a9..e8106e1f215008 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -88,6 +88,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + 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 ca840028bdf975..bf0d5170468773 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 @@ -694,29 +694,7 @@ internal bool TryGetValue(int index, out Guid value) ReadOnlySpan data = _utf8Json.Span; ReadOnlySpan segment = data.Slice(row.Location, row.SizeOrLength); - if (segment.Length > JsonConstants.MaximumEscapedGuidLength) - { - value = default; - return false; - } - - // Segment needs to be unescaped - if (row.HasComplexChildren) - { - return JsonReaderHelper.TryGetEscapedGuid(segment, out value); - } - - Debug.Assert(segment.IndexOf(JsonConstants.BackSlash) == -1); - - if (segment.Length == JsonConstants.MaximumFormatGuidLength - && Utf8Parser.TryParse(segment, out Guid tmp, out _, 'D')) - { - value = tmp; - return true; - } - - value = default; - return false; + return JsonReaderHelper.TryGetValue(segment, row.HasComplexChildren, out value); } internal string GetRawValueAsString(int index) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs new file mode 100644 index 00000000000000..379eafb8e13d90 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs @@ -0,0 +1,318 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Buffers.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text.Encodings.Web; + +namespace System.Text.Json.Nodes +{ + internal static class JsonValueOfJsonPrimitive + { + internal static JsonValue CreatePrimitiveValue(ref Utf8JsonReader reader, JsonNodeOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.False: + case JsonTokenType.True: + return new JsonValueOfJsonBool(reader.GetBoolean(), options); + case JsonTokenType.String: + byte[] buffer = new byte[reader.ValueLength]; + ReadOnlyMemory utf8String = buffer.AsMemory(0, reader.CopyString(buffer)); + return new JsonValueOfJsonString(utf8String, options); + case JsonTokenType.Number: + byte[] numberValue = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray(); + return new JsonValueOfJsonNumber(numberValue, options); + default: + Debug.Fail("Only primitives allowed."); + ThrowHelper.ThrowJsonException(); + return null!; // Unreachable, but required for compilation. + } + } + + private sealed class JsonValueOfJsonString : JsonValue + { + private readonly ReadOnlyMemory _value; + + internal JsonValueOfJsonString(ReadOnlyMemory utf8String, JsonNodeOptions? options) + : base(options) + { + _value = utf8String; + } + + internal override JsonNode DeepCloneCore() => new JsonValueOfJsonString(_value, Options); + private protected override JsonValueKind GetValueKindCore() => JsonValueKind.String; + + public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null) + { + ArgumentNullException.ThrowIfNull(writer); + + writer.WriteStringValue(_value.Span); + } + + public override T GetValue() + { + if (!TryGetValue(out T? value)) + { + ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvertElement(JsonValueKind.String, typeof(T)); + } + + return value; + } + + public override bool TryGetValue([NotNullWhen(true)] out T? value) + where T : default + { + if (typeof(T) == typeof(JsonElement)) + { + value = (T)(object)JsonWriterHelper.WriteString(_value.Span, static serialized => JsonElement.Parse(serialized)); + return true; + } + + if (typeof(T) == typeof(string)) + { + string? result = JsonReaderHelper.TranscodeHelper(_value.Span); + + Debug.Assert(result != null); + value = (T)(object)result; + return true; + } + + bool success; + + if (typeof(T) == typeof(DateTime) || typeof(T) == typeof(DateTime?)) + { + success = JsonReaderHelper.TryGetValue(_value.Span, isEscaped: false, out DateTime result); + value = (T)(object)result; + return success; + } + + if (typeof(T) == typeof(DateTimeOffset) || typeof(T) == typeof(DateTimeOffset?)) + { + success = JsonReaderHelper.TryGetValue(_value.Span, isEscaped: false, out DateTimeOffset result); + value = (T)(object)result; + return success; + } + + if (typeof(T) == typeof(Guid) || typeof(T) == typeof(Guid?)) + { + success = JsonReaderHelper.TryGetValue(_value.Span, isEscaped: false, out Guid result); + value = (T)(object)result; + return success; + } + + if (typeof(T) == typeof(char) || typeof(T) == typeof(char?)) + { + string? result = JsonReaderHelper.TranscodeHelper(_value.Span); + + Debug.Assert(result != null); + if (result.Length == 1) + { + value = (T)(object)result[0]; + return true; + } + } + + value = default!; + return false; + } + } + + private sealed class JsonValueOfJsonBool : JsonValue + { + private readonly bool _value; + + private JsonValueKind ValueKind => _value ? JsonValueKind.True : JsonValueKind.False; + + internal JsonValueOfJsonBool(bool value, JsonNodeOptions? options) + : base(options) + { + _value = value; + } + + public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null) => writer.WriteBooleanValue(_value); + internal override JsonNode DeepCloneCore() => new JsonValueOfJsonBool(_value, Options); + private protected override JsonValueKind GetValueKindCore() => ValueKind; + + public override T GetValue() + { + if (!TryGetValue(out T? value)) + { + ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvertElement(_value ? JsonValueKind.True : JsonValueKind.False, typeof(T)); + } + + return value; + } + + public override bool TryGetValue([NotNullWhen(true)] out T? value) + where T : default + { + if (typeof(T) == typeof(JsonElement)) + { + value = (T)(object)JsonElement.Parse(_value ? JsonConstants.TrueValue : JsonConstants.FalseValue); + return true; + } + + if (typeof(T) == typeof(bool) || typeof(T) == typeof(bool?)) + { + value = (T)(object)_value; + return true; + } + + value = default!; + return false; + } + } + + private sealed class JsonValueOfJsonNumber : JsonValue + { + // This can be optimized to store the decimal point position and the exponent so that + // conversion to different numeric types can be done without parsing the string again. + // Utf8Parser uses an internal ref struct, Number.NumberBuffer, which is really the + // same functionality that we would want here. + private readonly byte[] _value; + + internal JsonValueOfJsonNumber(byte[] number, JsonNodeOptions? options) + : base(options) + { + _value = number; + } + + internal override JsonNode DeepCloneCore() => new JsonValueOfJsonNumber(_value, Options); + private protected override JsonValueKind GetValueKindCore() => JsonValueKind.Number; + + public override T GetValue() + { + if (!TryGetValue(out T? value)) + { + ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvertElement(JsonValueKind.Number, typeof(T)); + } + + return value; + } + + public override bool TryGetValue([NotNullWhen(true)] out T? value) + where T : default + { + if (typeof(T) == typeof(JsonElement)) + { + value = (T)(object)JsonElement.Parse(_value); + return true; + } + + bool success; + + if (typeof(T) == typeof(int) || typeof(T) == typeof(int?)) + { + success = Utf8Parser.TryParse(_value, out int result, out int consumed) && + consumed == _value.Length; + + value = (T)(object)result; + return success; + } + + if (typeof(T) == typeof(long) || typeof(T) == typeof(long?)) + { + success = Utf8Parser.TryParse(_value, out long result, out int consumed) && + consumed == _value.Length; + + value = (T)(object)result; + return success; + } + + if (typeof(T) == typeof(double) || typeof(T) == typeof(double?)) + { + success = Utf8Parser.TryParse(_value, out double result, out int consumed) && + consumed == _value.Length; + + value = (T)(object)result; + return success; + } + + if (typeof(T) == typeof(short) || typeof(T) == typeof(short?)) + { + success = Utf8Parser.TryParse(_value, out short result, out int consumed) && + consumed == _value.Length; + + value = (T)(object)result; + return success; + } + + if (typeof(T) == typeof(decimal) || typeof(T) == typeof(decimal?)) + { + success = Utf8Parser.TryParse(_value, out decimal result, out int consumed) && + consumed == _value.Length; + + value = (T)(object)result; + return success; + } + + if (typeof(T) == typeof(byte) || typeof(T) == typeof(byte?)) + { + success = Utf8Parser.TryParse(_value, out byte result, out int consumed) && + consumed == _value.Length; + + value = (T)(object)result; + return success; + } + + if (typeof(T) == typeof(float) || typeof(T) == typeof(float?)) + { + success = Utf8Parser.TryParse(_value, out float result, out int consumed) && + consumed == _value.Length; + + value = (T)(object)result; + return success; + } + + if (typeof(T) == typeof(uint) || typeof(T) == typeof(uint?)) + { + success = Utf8Parser.TryParse(_value, out uint result, out int consumed) && + consumed == _value.Length; + + value = (T)(object)result; + return success; + } + + if (typeof(T) == typeof(ushort) || typeof(T) == typeof(ushort?)) + { + success = Utf8Parser.TryParse(_value, out ushort result, out int consumed) && + consumed == _value.Length; + + value = (T)(object)result; + return success; + } + + if (typeof(T) == typeof(ulong) || typeof(T) == typeof(ulong?)) + { + success = Utf8Parser.TryParse(_value, out ulong result, out int consumed) && + consumed == _value.Length; + + value = (T)(object)result; + return success; + } + + if (typeof(T) == typeof(sbyte) || typeof(T) == typeof(sbyte?)) + { + success = Utf8Parser.TryParse(_value, out sbyte result, out int consumed) && + consumed == _value.Length; + + value = (T)(object)result; + return success; + } + + value = default!; + return false; + } + + public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null) + { + ArgumentNullException.ThrowIfNull(writer); + + writer.WriteNumberValue(_value); + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs index 3c748e8fbb988b..bc27b3b0203f89 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs @@ -175,6 +175,33 @@ public static bool TryGetEscapedDateTimeOffset(ReadOnlySpan source, out Da return false; } + public static bool TryGetValue(ReadOnlySpan segment, bool isEscaped, out Guid value) + { + if (segment.Length > JsonConstants.MaximumEscapedGuidLength) + { + value = default; + return false; + } + + // Segment needs to be unescaped + if (isEscaped) + { + return TryGetEscapedGuid(segment, out value); + } + + Debug.Assert(segment.IndexOf(JsonConstants.BackSlash) == -1); + + if (segment.Length == JsonConstants.MaximumFormatGuidLength + && Utf8Parser.TryParse(segment, out Guid tmp, out _, 'D')) + { + value = tmp; + return true; + } + + value = default; + return false; + } + public static bool TryGetEscapedGuid(ReadOnlySpan source, out Guid value) { Debug.Assert(source.Length <= JsonConstants.MaximumEscapedGuidLength); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs index 5d59810e106b4d..8b07f85451bf11 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs @@ -1360,22 +1360,7 @@ internal bool TryGetGuidCore(out Guid value) span = ValueSpan; } - if (ValueIsEscaped) - { - return JsonReaderHelper.TryGetEscapedGuid(span, out value); - } - - Debug.Assert(span.IndexOf(JsonConstants.BackSlash) == -1); - - if (span.Length == JsonConstants.MaximumFormatGuidLength - && Utf8Parser.TryParse(span, out Guid tmp, out _, 'D')) - { - value = tmp; - return true; - } - - value = default; - return false; + return JsonReaderHelper.TryGetValue(span, ValueIsEscaped, out value); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs index bf3e26482a4413..ff7608298514d7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs @@ -43,23 +43,7 @@ public override void Write(Utf8JsonWriter writer, JsonValue? value, JsonSerializ internal static JsonValue ReadNonNullPrimitiveValue(ref Utf8JsonReader reader, JsonNodeOptions options) { Debug.Assert(reader.TokenType is JsonTokenType.String or JsonTokenType.False or JsonTokenType.True or JsonTokenType.Number); - - switch (reader.TokenType) - { - case JsonTokenType.String: - return JsonValue.Create(reader.GetString()!, options); - case JsonTokenType.False: - return JsonValue.Create(false, options); - case JsonTokenType.True: - return JsonValue.Create(true, options); - case JsonTokenType.Number: - // We can't infer CLR type for the number, so we parse it as a JsonElement. - JsonElement element = JsonElement.ParseValue(ref reader); - return JsonValue.CreateFromElement(ref element, options)!; - default: - Debug.Fail("Unexpected token type for primitive value."); - throw new JsonException(); - } + return JsonValueOfJsonPrimitive.CreatePrimitiveValue(ref reader, options); } internal override JsonSchema? GetSchema(JsonNumberHandling _) => JsonSchema.CreateTrueSchema(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.cs index 8844132888beee..1ab10d31f53b21 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Text.Encodings.Web; using System.Text.Unicode; namespace System.Text.Json @@ -322,5 +323,73 @@ internal static OperationStatus ToUtf8(ReadOnlySpan source, Span des } #endif } + + internal delegate T WriteCallback(ReadOnlySpan serializedValue); + + internal static T WriteString(ReadOnlySpan utf8Value, WriteCallback writeCallback) + { + int firstByteToEscape = JsonWriterHelper.NeedsEscaping(utf8Value, JavaScriptEncoder.Default); + + if (firstByteToEscape == -1) + { + int quotedLength = utf8Value.Length + 2; + byte[]? rented = null; + + try + { + Span quotedValue = quotedLength > JsonConstants.StackallocByteThreshold + ? (rented = ArrayPool.Shared.Rent(quotedLength)).AsSpan(0, quotedLength) + : stackalloc byte[JsonConstants.StackallocByteThreshold].Slice(0, quotedLength); + + quotedValue[0] = JsonConstants.Quote; + utf8Value.CopyTo(quotedValue.Slice(1)); + quotedValue[quotedValue.Length - 1] = JsonConstants.Quote; + + return writeCallback(quotedValue); + } + finally + { + if (rented != null) + { + ArrayPool.Shared.Return(rented); + } + } + } + else + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8Value.Length); + + int length = checked(2 + JsonWriterHelper.GetMaxEscapedLength(utf8Value.Length, firstByteToEscape)); + byte[]? rented = null; + + try + { + scoped Span escapedValue; + + if (length > JsonConstants.StackallocByteThreshold) + { + rented = ArrayPool.Shared.Rent(length); + escapedValue = rented; + } + else + { + escapedValue = stackalloc byte[JsonConstants.StackallocByteThreshold]; + } + + escapedValue[0] = JsonConstants.Quote; + JsonWriterHelper.EscapeString(utf8Value, escapedValue.Slice(1), firstByteToEscape, JavaScriptEncoder.Default, out int written); + escapedValue[1 + written] = JsonConstants.Quote; + + return writeCallback(escapedValue.Slice(0, written + 2)); + } + finally + { + if (rented != null) + { + ArrayPool.Shared.Return(rented); + } + } + } + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs index 0719fa58414a59..83a51a3b3a7aaa 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs @@ -1,11 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using System.Text.Json.Tests; using Xunit; namespace System.Text.Json.Nodes.Tests @@ -263,6 +265,68 @@ public static void FromElement_ToElement(string json, string expected) } } + [Theory] + [InlineData("\"A\"", "A")] + [InlineData("\"AB\"", "AB")] + [InlineData("\"A\\u0022\"", "A\"")] // ValueEquals compares unescaped values + public static void DeserializePrimitive_ToElement_String(string json, string expected) + { + DoTest(json, expected); + + // Test long strings + string padding = new string('P', 256); + DoTest(json[0] + padding + json.Substring(1), padding + expected); + + static void DoTest(string json, string expected) + { + JsonValue value = JsonSerializer.Deserialize(json); + JsonElement element = value.GetValue(); + + AssertExtensions.TrueExpression(element.ValueEquals(expected)); + + bool success = value.TryGetValue(out element); + Assert.True(success); + AssertExtensions.TrueExpression(element.ValueEquals(expected)); + } + } + + [Fact] + public static void DeserializePrimitive_ToElement_Bool() + { + // true + JsonValue value = JsonSerializer.Deserialize("true"); + + JsonElement element = value.GetValue(); + Assert.Equal(JsonValueKind.True, element.ValueKind); + + bool success = value.TryGetValue(out element); + Assert.True(success); + Assert.Equal(JsonValueKind.True, element.ValueKind); + + // false + value = JsonSerializer.Deserialize("false"); + + element = value.GetValue(); + Assert.Equal(JsonValueKind.False, element.ValueKind); + + success = value.TryGetValue(out element); + Assert.True(success); + Assert.Equal(JsonValueKind.False, element.ValueKind); + } + + [Fact] + public static void DeserializePrimitive_ToElement_Number() + { + JsonValue value = JsonSerializer.Deserialize("42"); + + JsonElement element = value.GetValue(); + Assert.Equal(42, element.GetInt32()); + + bool success = value.TryGetValue(out element); + Assert.True(success); + Assert.Equal(42, element.GetInt32()); + } + [Theory] [InlineData("42")] [InlineData("\"AB\"")] @@ -309,6 +373,26 @@ public static void WriteTo() Assert.Equal(Json, json); } + [Theory] + [InlineData("\"A\"")] + [InlineData("\"AB\"")] + [InlineData("\"A\\u0022\"")] + [InlineData("42")] + [InlineData("true")] + [InlineData("false")] + public static void DeserializePrimitive_WriteTo(string json) + { + byte[] utf8Json = Encoding.UTF8.GetBytes(json); + JsonValue value = JsonSerializer.Deserialize(utf8Json); + + var buffer = new ArrayBufferWriter(json.Length); + using Utf8JsonWriter writer = new Utf8JsonWriter(buffer); + value.WriteTo(writer); + writer.Flush(); + + AssertExtensions.SequenceEqual(utf8Json, buffer.WrittenSpan); + } + [Fact] public static void DeepCloneNotTrimmable() { @@ -547,24 +631,27 @@ private class Student [Theory] [MemberData(nameof(GetPrimitiveTypes))] - public static void PrimitiveTypes_ReturnExpectedTypeKind(T value, JsonValueKind expectedKind) + public static void PrimitiveTypes_ReturnExpectedTypeKind(WrappedT wrapped, JsonValueKind expectedKind) { + T value = wrapped.Value; JsonNode node = JsonValue.Create(value); Assert.Equal(expectedKind, node.GetValueKind()); } [Theory] [MemberData(nameof(GetPrimitiveTypes))] - public static void PrimitiveTypes_EqualThemselves(T value, JsonValueKind _) + public static void PrimitiveTypes_EqualThemselves(WrappedT wrapped, JsonValueKind _) { + T value = wrapped.Value; JsonNode node = JsonValue.Create(value); Assert.True(JsonNode.DeepEquals(node, node)); } [Theory] [MemberData(nameof(GetPrimitiveTypes))] - public static void PrimitiveTypes_EqualClonedValue(T value, JsonValueKind _) + public static void PrimitiveTypes_EqualClonedValue(WrappedT wrapped, JsonValueKind _) { + T value = wrapped.Value; JsonNode node = JsonValue.Create(value); JsonNode clone = node.DeepClone(); @@ -575,8 +662,9 @@ public static void PrimitiveTypes_EqualClonedValue(T value, JsonValueKind _) [Theory] [MemberData(nameof(GetPrimitiveTypes))] - public static void PrimitiveTypes_EqualDeserializedValue(T value, JsonValueKind _) + public static void PrimitiveTypes_EqualDeserializedValue(WrappedT wrapped, JsonValueKind _) { + T value = wrapped.Value; JsonNode node = JsonValue.Create(value); JsonNode clone = JsonSerializer.Deserialize(node.ToJsonString()); @@ -585,6 +673,100 @@ public static void PrimitiveTypes_EqualDeserializedValue(T value, JsonValueKi Assert.True(JsonNode.DeepEquals(clone, node)); } + [Theory] + [MemberData(nameof(GetPrimitiveTypes))] + public static void PrimitiveTypes_DeepEquals_DifferentRepresentations(WrappedT wrapped, JsonValueKind _) + { + T value = wrapped.Value; + string json = JsonSerializer.Serialize(value); + JsonNode node = JsonSerializer.Deserialize(json); + JsonNode other = JsonSerializer.Deserialize($"[{json}]")[0]; // JsonValueOfElement + + Assert.True(JsonNode.DeepEquals(other, other)); + Assert.True(JsonNode.DeepEquals(node, other)); + Assert.True(JsonNode.DeepEquals(other, node)); + } + + [Theory] + [MemberData(nameof(GetPrimitiveTypes))] + public static void PrimitiveTypes_EqualClonedValue_DeserializedValue(WrappedT wrapped, JsonValueKind _) + { + T value = wrapped.Value; + string json = JsonSerializer.Serialize(value); + JsonNode node = JsonSerializer.Deserialize(json); + JsonNode clone = node.DeepClone(); + + Assert.True(JsonNode.DeepEquals(clone, clone)); + Assert.True(JsonNode.DeepEquals(node, clone)); + Assert.True(JsonNode.DeepEquals(clone, node)); + } + + private static readonly HashSet s_convertibleTypes = + [ + // True/False + typeof(bool), typeof(bool?), + + // Number + typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal), + + typeof(byte?), typeof(sbyte?), typeof(short?), typeof(ushort?), typeof(int?), typeof(uint?), + typeof(long?), typeof(ulong?), typeof(float?), typeof(double?), typeof(decimal?), + + // String + typeof(char), typeof(char?), + typeof(string), + typeof(DateTimeOffset), typeof(DateTimeOffset?), + typeof(DateTime), typeof(DateTime?), + typeof(Guid), typeof(Guid?), + ]; + + [Theory] + [MemberData(nameof(GetPrimitiveTypes))] + public static void PrimitiveTypes_Conversion(WrappedT wrapped, JsonValueKind _) + { + T value = wrapped.Value; + string json = JsonSerializer.Serialize(value); + bool canGetValue = s_convertibleTypes.Contains(typeof(T)); + + JsonValue jsonValue = JsonSerializer.Deserialize(json)!; + AssertExtensions.TrueExpression(jsonValue.TryGetValue(out T unused) == canGetValue); + + if (canGetValue) + { + // Assert no throw + jsonValue.GetValue(); + } + else + { + Assert.Throws(() => jsonValue.GetValue()); + } + + JsonValue jsonNode = (JsonValue)JsonSerializer.Deserialize(json)!; + AssertExtensions.TrueExpression(jsonNode.TryGetValue(out unused) == canGetValue); + + // Ensure the eager evaluation code path also produces the same result + jsonNode = (JsonValue)JsonSerializer.Deserialize(json, new JsonSerializerOptions { AllowDuplicateProperties = false })!; + AssertExtensions.TrueExpression(jsonNode.TryGetValue(out unused) == canGetValue); + } + + [Theory] + [MemberData(nameof(GetPrimitiveTypes))] + public static void PrimitiveTypes_ReadOnlySequence(WrappedT wrapped, JsonValueKind _) + { + T value = wrapped.Value; + + byte[] jsonBytes = JsonSerializer.SerializeToUtf8Bytes(value); + ReadOnlySequence seq = BufferFactory.Create([jsonBytes.AsMemory(0, 1), jsonBytes.AsMemory(1, jsonBytes.Length - 1)]); + Utf8JsonReader reader = new Utf8JsonReader(seq); + JsonValue jsonValueFromSequence = JsonSerializer.Deserialize(ref reader)!; + + string jsonString = JsonSerializer.Serialize(value); + JsonValue jsonValueFromString = JsonSerializer.Deserialize(jsonString)!; + + AssertExtensions.TrueExpression(JsonNode.DeepEquals(jsonValueFromString, jsonValueFromSequence)); + } + public static IEnumerable GetPrimitiveTypes() { yield return Wrap(false, JsonValueKind.False); @@ -592,23 +774,37 @@ public static IEnumerable GetPrimitiveTypes() yield return Wrap((bool?)false, JsonValueKind.False); yield return Wrap((bool?)true, JsonValueKind.True); yield return Wrap((byte)42, JsonValueKind.Number); + yield return Wrap((byte?)42, JsonValueKind.Number); yield return Wrap((sbyte)42, JsonValueKind.Number); + yield return Wrap((sbyte?)42, JsonValueKind.Number); yield return Wrap((short)42, JsonValueKind.Number); + yield return Wrap((short?)42, JsonValueKind.Number); yield return Wrap((ushort)42, JsonValueKind.Number); + yield return Wrap((ushort?)42, JsonValueKind.Number); yield return Wrap(42, JsonValueKind.Number); yield return Wrap((int?)42, JsonValueKind.Number); yield return Wrap((uint)42, JsonValueKind.Number); + yield return Wrap((uint?)42, JsonValueKind.Number); yield return Wrap((long)42, JsonValueKind.Number); + yield return Wrap((long?)42, JsonValueKind.Number); yield return Wrap((ulong)42, JsonValueKind.Number); + yield return Wrap((ulong?)42, JsonValueKind.Number); yield return Wrap(42.0f, JsonValueKind.Number); + yield return Wrap((float?)42.0f, JsonValueKind.Number); yield return Wrap(42.0, JsonValueKind.Number); + yield return Wrap((double?)42.0, JsonValueKind.Number); yield return Wrap(42.0m, JsonValueKind.Number); + yield return Wrap((decimal?)42.0m, JsonValueKind.Number); yield return Wrap('A', JsonValueKind.String); yield return Wrap((char?)'A', JsonValueKind.String); yield return Wrap("A", JsonValueKind.String); + yield return Wrap("A\u0041", JsonValueKind.String); // \u0041 == A + yield return Wrap("A\u0022", JsonValueKind.String); // \u0022 == " yield return Wrap(new byte[] { 1, 2, 3 }, JsonValueKind.String); yield return Wrap(new DateTimeOffset(2024, 06, 20, 10, 29, 0, TimeSpan.Zero), JsonValueKind.String); + yield return Wrap((DateTimeOffset?)new DateTimeOffset(2024, 06, 20, 10, 29, 0, TimeSpan.Zero), JsonValueKind.String); yield return Wrap(new DateTime(2024, 06, 20, 10, 29, 0), JsonValueKind.String); + yield return Wrap((DateTime?)new DateTime(2024, 06, 20, 10, 29, 0), JsonValueKind.String); yield return Wrap(Guid.Empty, JsonValueKind.String); yield return Wrap((Guid?)Guid.Empty, JsonValueKind.String); yield return Wrap(new Uri("http://example.com"), JsonValueKind.String); @@ -624,7 +820,94 @@ public static IEnumerable GetPrimitiveTypes() yield return Wrap(new DateOnly(2024, 06, 20), JsonValueKind.String); yield return Wrap(new TimeOnly(10, 29), JsonValueKind.String); #endif - static object[] Wrap(T value, JsonValueKind expectedKind) => [value, expectedKind]; + static object[] Wrap(T value, JsonValueKind expectedKind) => [new WrappedT { Value = value }, expectedKind]; + } + + public class WrappedT + { + public T Value; + + public override string ToString() => Value?.ToString(); + } + + [Theory] + [InlineData("\"string\"")] + [InlineData("42.0")] + [InlineData("true")] + [InlineData("false")] + public static void PrimitiveTypes_ConverterThrows(string json) + { + JsonSerializerOptions opts = new JsonSerializerOptions + { + Converters = { new ThrowingConverter() } + }; + + JsonValue jsonValue = JsonSerializer.Deserialize(json, opts); + + Assert.False(jsonValue.TryGetValue(out DummyClass unused)); + Assert.Throws(() => jsonValue.GetValue()); } + + private sealed class ThrowingConverter : JsonConverter + { + public override DummyClass Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new JsonException(); + + public override void Write(Utf8JsonWriter writer, DummyClass value, JsonSerializerOptions options) => Assert.Fail(); + } + + [Theory] + [InlineData("\"string\"")] + [InlineData("42.0")] + [InlineData("true")] + [InlineData("false")] + public static void PrimitiveTypes_ConverterReturnsNull(string json) + { + JsonSerializerOptions opts = new JsonSerializerOptions + { + Converters = { new NullConverter() } + }; + + JsonValue jsonValue = JsonSerializer.Deserialize(json, opts); + + Assert.False(jsonValue.TryGetValue(out DummyClass unused)); + Assert.Throws(() => jsonValue.GetValue()); + } + + private sealed class NullConverter : JsonConverter + { + public override DummyClass Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => null; + + public override void Write(Utf8JsonWriter writer, DummyClass value, JsonSerializerOptions options) => Assert.Fail(); + } + + [Theory] + [InlineData("\"string\"")] + [InlineData("42.0")] + [InlineData("true")] + [InlineData("false")] + public static void PrimitiveTypes_NoTypeInfo(string json) + { + JsonSerializerOptions opts = new JsonSerializerOptions + { + TypeInfoResolver = new ExcludeType_TypeInfoResolver(typeof(DummyClass)) + }; + + JsonValue jsonValue = JsonSerializer.Deserialize(json, opts); + + Assert.False(jsonValue.TryGetValue(out DummyClass unused)); + Assert.Throws(() => jsonValue.GetValue()); + } + + private class ExcludeType_TypeInfoResolver(Type excludeType) : IJsonTypeInfoResolver + { + private static readonly DefaultJsonTypeInfoResolver _defaultResolver = new DefaultJsonTypeInfoResolver(); + + public Type ExcludeType { get; } = excludeType; + + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) => + type == ExcludeType ? null : _defaultResolver.GetTypeInfo(type, options); + } + + private record DummyClass; } }