diff --git a/src/libraries/System.Text.Json/Common/JsonHelpers.cs b/src/libraries/System.Text.Json/Common/JsonHelpers.cs index ad718eba4a41fe..56b1103e0b3611 100644 --- a/src/libraries/System.Text.Json/Common/JsonHelpers.cs +++ b/src/libraries/System.Text.Json/Common/JsonHelpers.cs @@ -26,6 +26,20 @@ public static bool TryAdd(this Dictionary dictionary return false; } + /// + /// netstandard/netfx polyfill for IDictionary.TryAdd + /// + public static bool TryAdd(this IDictionary dictionary, TKey key, TValue value) where TKey : notnull + { + if (!dictionary.ContainsKey(key)) + { + dictionary[key] = value; + return true; + } + + return false; + } + /// /// netstandard/netfx polyfill for Queue.TryDequeue /// diff --git a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs index 5f38b1985abd12..c254ed980ed506 100644 --- a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs +++ b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs @@ -170,5 +170,10 @@ public JsonSourceGenerationOptionsAttribute(JsonSerializerDefaults defaults) /// Specifies the default value of when set. /// public string? NewLine { get; set; } + + /// + /// Specifies the default value of when set. + /// + public bool AllowDuplicateProperties { get; set; } } } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index f8ff745cd4400b..80b648dfd95803 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -1188,6 +1188,9 @@ private static void GetLogicForDefaultSerializerOptionsInit(SourceGenerationOpti writer.WriteLine('{'); writer.Indentation++; + if (optionsSpec.AllowDuplicateProperties is bool allowDuplicateProperties) + writer.WriteLine($"AllowDuplicateProperties = {FormatBoolLiteral(allowDuplicateProperties)},"); + if (optionsSpec.AllowOutOfOrderMetadataProperties is bool allowOutOfOrderMetadataProperties) writer.WriteLine($"AllowOutOfOrderMetadataProperties = {FormatBoolLiteral(allowOutOfOrderMetadataProperties)},"); diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 4339851dce17cb..3e0a0673261b84 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -287,6 +287,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN bool? writeIndented = null; char? indentCharacter = null; int? indentSize = null; + bool? allowDuplicateProperties = null; if (attributeData.ConstructorArguments.Length > 0) { @@ -412,6 +413,10 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN generationMode = (JsonSourceGenerationMode)namedArg.Value.Value!; break; + case nameof(JsonSourceGenerationOptionsAttribute.AllowDuplicateProperties): + allowDuplicateProperties = (bool)namedArg.Value.Value!; + break; + default: throw new InvalidOperationException(); } @@ -446,6 +451,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN WriteIndented = writeIndented, IndentCharacter = indentCharacter, IndentSize = indentSize, + AllowDuplicateProperties = allowDuplicateProperties, }; } diff --git a/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs b/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs index 2f10b3b3f41ec3..c7f6156c1dac57 100644 --- a/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs @@ -66,6 +66,8 @@ public sealed record SourceGenerationOptionsSpec public required int? IndentSize { get; init; } + public required bool? AllowDuplicateProperties { get; init; } + public JsonKnownNamingPolicy? GetEffectivePropertyNamingPolicy() => PropertyNamingPolicy ?? (Defaults is JsonSerializerDefaults.Web ? JsonKnownNamingPolicy.CamelCase : null); } 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 0b1d60e5c8b4c1..616934dc9ac456 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -38,6 +38,7 @@ public void WriteTo(System.Text.Json.Utf8JsonWriter writer) { } public partial struct JsonDocumentOptions { private int _dummyPrimitive; + public bool AllowDuplicateProperties { get { throw null; } set { } } public bool AllowTrailingCommas { readonly get { throw null; } set { } } public System.Text.Json.JsonCommentHandling CommentHandling { readonly get { throw null; } set { } } public int MaxDepth { readonly get { throw null; } set { } } @@ -393,6 +394,7 @@ public sealed partial class JsonSerializerOptions public JsonSerializerOptions() { } public JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults defaults) { } public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { } + public bool AllowDuplicateProperties { get { throw null; } set { } } public bool AllowOutOfOrderMetadataProperties { get { throw null; } set { } } public bool AllowTrailingCommas { get { throw null; } set { } } public System.Collections.Generic.IList Converters { get { throw null; } } @@ -1139,6 +1141,7 @@ public sealed partial class JsonSourceGenerationOptionsAttribute : System.Text.J { public JsonSourceGenerationOptionsAttribute() { } public JsonSourceGenerationOptionsAttribute(System.Text.Json.JsonSerializerDefaults defaults) { } + public bool AllowDuplicateProperties { get { throw null; } set { } } public bool AllowOutOfOrderMetadataProperties { get { throw null; } set { } } public bool AllowTrailingCommas { get { throw null; } set { } } public System.Type[]? Converters { get { throw null; } set { } } diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 1a4f9421de95d9..906b4bc2b5c5b9 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -827,4 +827,13 @@ Mixing UTF encodings in a single multi-segment JSON string is not supported. The previous segment's encoding was '{0}' and the current segment's encoding is '{1}'. + + Duplicate property '{0}' encountered during deserialization of type '{1}'. + + + Duplicate property '{0}' encountered during deserialization. + + + Duplicate properties not allowed during deserialization. + 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 2d3b2af2e99d66..ae01e453ad574c 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -51,6 +51,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + @@ -342,6 +343,8 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + + @@ -418,6 +421,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.Parse.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs index 0d89711b5631d7..aff7a38afff34f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs @@ -46,7 +46,7 @@ public sealed partial class JsonDocument /// public static JsonDocument Parse(ReadOnlyMemory utf8Json, JsonDocumentOptions options = default) { - return Parse(utf8Json, options.GetReaderOptions()); + return Parse(utf8Json, options.GetReaderOptions(), allowDuplicateProperties: options.AllowDuplicateProperties); } /// @@ -80,7 +80,7 @@ public static JsonDocument Parse(ReadOnlySequence utf8Json, JsonDocumentOp if (utf8Json.IsSingleSegment) { - return Parse(utf8Json.First, readerOptions); + return Parse(utf8Json.First, readerOptions, allowDuplicateProperties: options.AllowDuplicateProperties); } int length = checked((int)utf8Json.Length); @@ -89,7 +89,11 @@ public static JsonDocument Parse(ReadOnlySequence utf8Json, JsonDocumentOp try { utf8Json.CopyTo(utf8Bytes.AsSpan()); - return Parse(utf8Bytes.AsMemory(0, length), readerOptions, utf8Bytes); + return Parse( + utf8Bytes.AsMemory(0, length), + readerOptions, + utf8Bytes, + allowDuplicateProperties: options.AllowDuplicateProperties); } catch { @@ -123,7 +127,11 @@ public static JsonDocument Parse(Stream utf8Json, JsonDocumentOptions options = Debug.Assert(drained.Array != null); try { - return Parse(drained.AsMemory(), options.GetReaderOptions(), drained.Array); + return Parse( + drained.AsMemory(), + options.GetReaderOptions(), + drained.Array, + allowDuplicateProperties: options.AllowDuplicateProperties); } catch { @@ -140,7 +148,8 @@ internal static JsonDocument ParseRented(PooledByteBufferWriter utf8Json, JsonDo utf8Json.WrittenMemory, options.GetReaderOptions(), extraRentedArrayPoolBytes: null, - extraPooledByteBufferWriter: utf8Json); + extraPooledByteBufferWriter: utf8Json, + allowDuplicateProperties: options.AllowDuplicateProperties); } internal static JsonDocument ParseValue(Stream utf8Json, JsonDocumentOptions options) @@ -157,7 +166,10 @@ internal static JsonDocument ParseValue(Stream utf8Json, JsonDocumentOptions opt drained.AsSpan().Clear(); ArrayPool.Shared.Return(drained.Array); - return ParseUnrented(owned.AsMemory(), options.GetReaderOptions()); + return ParseUnrented( + owned.AsMemory(), + options.GetReaderOptions(), + allowDuplicateProperties: options.AllowDuplicateProperties); } internal static JsonDocument ParseValue(ReadOnlySpan utf8Json, JsonDocumentOptions options) @@ -165,7 +177,10 @@ internal static JsonDocument ParseValue(ReadOnlySpan utf8Json, JsonDocumen byte[] owned = new byte[utf8Json.Length]; utf8Json.CopyTo(owned); - return ParseUnrented(owned.AsMemory(), options.GetReaderOptions()); + return ParseUnrented( + owned.AsMemory(), + options.GetReaderOptions(), + allowDuplicateProperties: options.AllowDuplicateProperties); } internal static JsonDocument ParseValue(string json, JsonDocumentOptions options) @@ -209,7 +224,11 @@ private static async Task ParseAsyncCore( Debug.Assert(drained.Array != null); try { - return Parse(drained.AsMemory(), options.GetReaderOptions(), drained.Array); + return Parse( + drained.AsMemory(), + options.GetReaderOptions(), + drained.Array, + allowDuplicateProperties: options.AllowDuplicateProperties); } catch { @@ -235,7 +254,10 @@ internal static async Task ParseAsyncCoreUnrented( drained.AsSpan().Clear(); ArrayPool.Shared.Return(drained.Array); - return ParseUnrented(owned.AsMemory(), options.GetReaderOptions()); + return ParseUnrented( + owned.AsMemory(), + options.GetReaderOptions(), + allowDuplicateProperties: options.AllowDuplicateProperties); } /// @@ -271,7 +293,8 @@ public static JsonDocument Parse([StringSyntax(StringSyntaxAttribute.Json)] Read return Parse( utf8Bytes.AsMemory(0, actualByteCount), options.GetReaderOptions(), - utf8Bytes); + utf8Bytes, + allowDuplicateProperties: options.AllowDuplicateProperties); } catch { @@ -304,7 +327,10 @@ internal static JsonDocument ParseValue(ReadOnlyMemory json, JsonDocumentO ArrayPool.Shared.Return(utf8Bytes); } - return ParseUnrented(owned.AsMemory(), options.GetReaderOptions()); + return ParseUnrented( + owned.AsMemory(), + options.GetReaderOptions(), + allowDuplicateProperties: options.AllowDuplicateProperties); } /// @@ -406,9 +432,12 @@ public static bool TryParseValue(ref Utf8JsonReader reader, [NotNullWhen(true)] /// /// A value could not be read from the reader. /// - public static JsonDocument ParseValue(ref Utf8JsonReader reader) + public static JsonDocument ParseValue(ref Utf8JsonReader reader) => + ParseValue(ref reader, allowDuplicateProperties: true); + + internal static JsonDocument ParseValue(ref Utf8JsonReader reader, bool allowDuplicateProperties) { - bool ret = TryParseValue(ref reader, out JsonDocument? document, shouldThrow: true, useArrayPools: true); + bool ret = TryParseValue(ref reader, out JsonDocument? document, shouldThrow: true, useArrayPools: true, allowDuplicateProperties); Debug.Assert(ret, "TryParseValue returned false with shouldThrow: true."); Debug.Assert(document != null, "null document returned with shouldThrow: true."); @@ -419,7 +448,8 @@ internal static bool TryParseValue( ref Utf8JsonReader reader, [NotNullWhen(true)] out JsonDocument? document, bool shouldThrow, - bool useArrayPools) + bool useArrayPools, + bool allowDuplicateProperties = true) { JsonReaderState state = reader.CurrentState; CheckSupportedOptions(state.Options, nameof(reader)); @@ -629,7 +659,7 @@ internal static bool TryParseValue( valueSpan.CopyTo(rentedSpan); } - document = Parse(rented.AsMemory(0, length), state.Options, rented); + document = Parse(rented.AsMemory(0, length), state.Options, rented, allowDuplicateProperties: allowDuplicateProperties); } catch { @@ -654,7 +684,7 @@ internal static bool TryParseValue( owned = valueSpan.ToArray(); } - document = ParseUnrented(owned, state.Options, reader.TokenType); + document = ParseUnrented(owned, state.Options, reader.TokenType, allowDuplicateProperties: allowDuplicateProperties); } return true; @@ -688,18 +718,28 @@ private static JsonDocument Parse( ReadOnlyMemory utf8Json, JsonReaderOptions readerOptions, byte[]? extraRentedArrayPoolBytes = null, - PooledByteBufferWriter? extraPooledByteBufferWriter = null) + PooledByteBufferWriter? extraPooledByteBufferWriter = null, + bool allowDuplicateProperties = true) { ReadOnlySpan utf8JsonSpan = utf8Json.Span; var database = MetadataDb.CreateRented(utf8Json.Length, convertToAlloc: false); var stack = new StackRowStack(JsonDocumentOptions.DefaultMaxDepth * StackRow.Size); + JsonDocument document; try { Parse(utf8JsonSpan, readerOptions, ref database, ref stack); + document = new JsonDocument(utf8Json, database, extraRentedArrayPoolBytes, extraPooledByteBufferWriter, isDisposable: true); + + if (!allowDuplicateProperties) + { + ValidateNoDuplicateProperties(document); + } } catch { + // The caller returns any resources they rented, so all we need to do is dispose the database. + // Specifically: don't dispose the document as that will result in double return of the rented array. database.Dispose(); throw; } @@ -708,13 +748,14 @@ private static JsonDocument Parse( stack.Dispose(); } - return new JsonDocument(utf8Json, database, extraRentedArrayPoolBytes, extraPooledByteBufferWriter); + return document; } private static JsonDocument ParseUnrented( ReadOnlyMemory utf8Json, JsonReaderOptions readerOptions, - JsonTokenType tokenType = JsonTokenType.None) + JsonTokenType tokenType = JsonTokenType.None, + bool allowDuplicateProperties = true) { // These tokens should already have been processed. Debug.Assert( @@ -746,7 +787,14 @@ private static JsonDocument ParseUnrented( } } - return new JsonDocument(utf8Json, database, isDisposable: false); + JsonDocument document = new JsonDocument(utf8Json, database, isDisposable: false); + + if (!allowDuplicateProperties) + { + ValidateNoDuplicateProperties(document); + } + + return document; } private static ArraySegment ReadToEnd(Stream stream) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.PropertyNameSet.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.PropertyNameSet.cs new file mode 100644 index 00000000000000..4aa736ef631337 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.PropertyNameSet.cs @@ -0,0 +1,161 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System.Text.Json +{ + public sealed partial class JsonDocument + { + // Ensure this stays on the stack by making it a ref struct. + private ref struct PropertyNameSet : IDisposable + { + // Data structure to track property names in an object while deserializing + // into a JsonDocument and validate that there are no duplicates. A small + // array is used when the number of properties is small and no properties + // are escaped. Otherwise a hash set is used. + + private HashSet>? _hashSet; + + private const int ArraySetThreshold = 16; + private int _arraySetCount; + private bool _useArraySet = true; + +#if NET + [InlineArray(ArraySetThreshold)] + private struct InlineRangeArray16 + { + private (int Start, int Length) _element0; + } + + private InlineRangeArray16 _arraySet; +#else + private readonly (int Start, int Length)[] _arraySet = new (int Start, int Length)[ArraySetThreshold]; +#endif + + public PropertyNameSet() + { + } + + internal void SetCapacity(int capacity) + { + if (capacity <= ArraySetThreshold) + { + _useArraySet = true; + } + else + { + _useArraySet = false; + if (_hashSet is null) + { + _hashSet = new HashSet>( +#if NET + capacity, +#endif + PropertyNameComparer.Instance); + } + else + { +#if NET + _hashSet.EnsureCapacity(capacity); +#endif + } + } + } + + internal void AddPropertyName(JsonProperty property, JsonDocument document) + { + DbRow dbRow = document._parsedData.Get(property.Value.MetadataDbIndex - DbRow.Size); + Debug.Assert(dbRow.TokenType is JsonTokenType.PropertyName); + + ReadOnlyMemory utf8Json = document._utf8Json; + ReadOnlyMemory propertyName = utf8Json.Slice(dbRow.Location, dbRow.SizeOrLength); + + if (dbRow.HasComplexChildren) + { + SwitchToHashSet(utf8Json); + propertyName = JsonReaderHelper.GetUnescaped(propertyName.Span); + } + + if (_useArraySet) + { + for (int i = 0; i < _arraySetCount; i++) + { + (int Start, int Length) range = _arraySet[i]; + ReadOnlySpan previousPropertyName = utf8Json.Span.Slice(range.Start, range.Length); + + if (previousPropertyName.SequenceEqual(propertyName.Span)) + { + ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(propertyName.Span); + } + } + + _arraySet[_arraySetCount] = (dbRow.Location, dbRow.SizeOrLength); + _arraySetCount++; + } + else + { + Debug.Assert(_hashSet is not null); + + if (!_hashSet.Add(propertyName)) + { + ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(propertyName.Span); + } + } + } + + private void SwitchToHashSet(ReadOnlyMemory utf8Json) + { + if (_useArraySet) + { + _hashSet ??= new HashSet>( +#if NET + ArraySetThreshold, +#endif + PropertyNameComparer.Instance); + + for (int i = 0; i < _arraySetCount; i++) + { + (int Start, int Length) range = _arraySet[i]; + ReadOnlyMemory propertyName = utf8Json.Slice(range.Start, range.Length); + bool success = _hashSet.Add(propertyName); + Debug.Assert(success, $"Property name {propertyName} should not already exist in the set."); + } + + _useArraySet = false; + _arraySetCount = 0; + } + } + + internal void Reset() + { + _hashSet?.Clear(); + _arraySetCount = 0; + } + + public readonly void Dispose() + { + } + + private sealed class PropertyNameComparer : IEqualityComparer> + { + internal static readonly PropertyNameComparer Instance = new(); + + public bool Equals(ReadOnlyMemory left, ReadOnlyMemory right) => + left.Length == right.Length && left.Span.SequenceEqual(right.Span); + + public int GetHashCode(ReadOnlyMemory name) + { + // Marvin is the currently used hash algorithm for string comparisons. + // The seed is unique to this process so an item's hash code can't easily be + // discovered by an adversary trying to perform a denial of service attack. + return Marvin.ComputeHash32(name.Span, Marvin.DefaultSeed); + } + } + } + } +} 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 75b177ecb28391..e4b1ea2d30ddb0 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 @@ -3,9 +3,9 @@ using System.Buffers; using System.Buffers.Text; +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; using System.Threading; namespace System.Text.Json @@ -1096,6 +1096,86 @@ private static void Parse( database.CompleteAllocations(); } + private static void ValidateNoDuplicateProperties(JsonDocument document) + { + if (document.RootElement.ValueKind is JsonValueKind.Array or JsonValueKind.Object) + { + ValidateDuplicatePropertiesCore(document); + } + } + + private static void ValidateDuplicatePropertiesCore(JsonDocument document) + { + Debug.Assert(document.RootElement.ValueKind is JsonValueKind.Array or JsonValueKind.Object); + + using PropertyNameSet propertyNameSet = new PropertyNameSet(); + + Stack traversalPath = new Stack(); + int? databaseIndexOflastProcessedChild = null; + + traversalPath.Push(document.RootElement.MetadataDbIndex); + + do + { + JsonElement curr = new JsonElement(document, traversalPath.Peek()); + + switch (curr.ValueKind) + { + case JsonValueKind.Object: + { + JsonElement.ObjectEnumerator enumerator = new(curr, databaseIndexOflastProcessedChild ?? -1); + + while (enumerator.MoveNext()) + { + if (enumerator.Current.Value.ValueKind is JsonValueKind.Object or JsonValueKind.Array) + { + traversalPath.Push(enumerator.Current.Value.MetadataDbIndex); + databaseIndexOflastProcessedChild = null; + goto continueOuter; + } + } + + // No more children, so process the current element. + enumerator.Reset(); + propertyNameSet.SetCapacity(curr.GetPropertyCount()); + + foreach (JsonProperty property in enumerator) + { + propertyNameSet.AddPropertyName(property, document); + } + + propertyNameSet.Reset(); + databaseIndexOflastProcessedChild = traversalPath.Pop(); + break; + } + case JsonValueKind.Array: + { + JsonElement.ArrayEnumerator enumerator = new(curr, databaseIndexOflastProcessedChild ?? -1); + + while (enumerator.MoveNext()) + { + if (enumerator.Current.ValueKind is JsonValueKind.Object or JsonValueKind.Array) + { + traversalPath.Push(enumerator.Current.MetadataDbIndex); + databaseIndexOflastProcessedChild = null; + goto continueOuter; + } + } + + databaseIndexOflastProcessedChild = traversalPath.Pop(); + break; + } + default: + Debug.Fail($"Expected only complex children but got {curr.ValueKind}"); + ThrowHelper.ThrowJsonException(); + break; + } + + continueOuter: + ; + } while (traversalPath.Count is not 0); + } + private void CheckNotDisposed() { if (_utf8Json.IsEmpty) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocumentOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocumentOptions.cs index df1289c295d3b9..a68051d636ea3e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocumentOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocumentOptions.cs @@ -69,6 +69,23 @@ public int MaxDepth /// public bool AllowTrailingCommas { get; set; } + /// + /// Defines whether duplicate property names are allowed when deserializing JSON objects. + /// + /// + /// + /// By default, it's set to true. If set to false, is thrown + /// when a duplicate property name is encountered during deserialization. + /// + /// + public bool AllowDuplicateProperties + { + // These are negated because the declaring type is a struct and we want the value to be true + // for the default struct value. + get => !field; + set => field = !value; + } + internal JsonReaderOptions GetReaderOptions() { return new JsonReaderOptions diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.ArrayEnumerator.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.ArrayEnumerator.cs index ee324cda04fb31..68cc99cf2a1c20 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.ArrayEnumerator.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.ArrayEnumerator.cs @@ -19,10 +19,10 @@ public struct ArrayEnumerator : IEnumerable, IEnumerator, IEnumerator _idx; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => $"ValueKind = {ValueKind} : \"{ToString()}\""; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs index f16e637835cf48..46c5b240b8c0db 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Text.Json.Nodes; using System.Text.RegularExpressions; namespace System.Text.Json @@ -22,7 +23,7 @@ public static ReadOnlySpan GetUnescapedSpan(this scoped ref Utf8JsonReader { Debug.Assert(reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName); ReadOnlySpan span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; - return reader.ValueIsEscaped ? JsonReaderHelper.GetUnescapedSpan(span) : span; + return reader.ValueIsEscaped ? JsonReaderHelper.GetUnescaped(span) : span; } /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs index ba611933eeee96..192e2f86481216 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization.Converters; using System.Text.Json.Serialization.Metadata; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs index 63f69942b3b7ad..60768cab4d44dc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs @@ -66,7 +66,7 @@ public static string GetUnescapedString(ReadOnlySpan utf8Source) return utf8String; } - public static ReadOnlySpan GetUnescapedSpan(ReadOnlySpan utf8Source) + public static byte[] GetUnescaped(ReadOnlySpan utf8Source) { // The escaped name is always >= than the unescaped, so it is safe to use escaped name for the buffer length. int length = utf8Source.Length; @@ -79,8 +79,8 @@ public static ReadOnlySpan GetUnescapedSpan(ReadOnlySpan utf8Source) Unescape(utf8Source, utf8Unescaped, out int written); Debug.Assert(written > 0); - ReadOnlySpan propertyName = utf8Unescaped.Slice(0, written).ToArray(); - Debug.Assert(!propertyName.IsEmpty); + byte[] propertyName = utf8Unescaped.Slice(0, written).ToArray(); + Debug.Assert(propertyName.Length is not 0); if (pooledName != null) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs index c2f9dbcfa88a16..eea091212e14d7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs @@ -2582,7 +2582,7 @@ private ReadOnlySpan GetUnescapedSpan() ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; if (ValueIsEscaped) { - span = JsonReaderHelper.GetUnescapedSpan(span); + span = JsonReaderHelper.GetUnescaped(span); } return span; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs index 07341507be684e..3d26a18d54ab8b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs @@ -19,7 +19,19 @@ internal sealed class DictionaryOfTKeyTValueConverter protected override void Add(TKey key, in TValue value, JsonSerializerOptions options, ref ReadStack state) { - ((TCollection)state.Current.ReturnValue!)[key] = value; + TCollection dictionary = (TCollection)state.Current.ReturnValue!; + + if (options.AllowDuplicateProperties) + { + dictionary[key] = value; + } + else + { + if (!dictionary.TryAdd(key, value)) + { + ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(); + } + } } protected internal override bool OnWriteResume( diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs index 4ea31cc48ad34a..043ff221fe8ad1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs @@ -21,7 +21,14 @@ internal sealed class IDictionaryConverter protected override void Add(string key, in object? value, JsonSerializerOptions options, ref ReadStack state) { TDictionary collection = (TDictionary)state.Current.ReturnValue!; + + if (!options.AllowDuplicateProperties && collection.Contains(key)) + { + ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(key); + } + collection[key] = value; + if (IsValueType) { state.Current.ReturnValue = collection; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfTKeyTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfTKeyTValueConverter.cs index fb1a1b6d01bafe..e68f9e265333d6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfTKeyTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfTKeyTValueConverter.cs @@ -21,7 +21,19 @@ internal sealed class IDictionaryOfTKeyTValueConverter)state.Current.ReturnValue!)[key] = value; + Dictionary dictionary = (Dictionary)state.Current.ReturnValue!; + + if (options.AllowDuplicateProperties) + { + dictionary[key] = value; + } + else + { + if (!dictionary.TryAdd(key, value)) + { + ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(); + } + } } internal override bool SupportsCreateObjectDelegate => false; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfTKeyTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfTKeyTValueConverter.cs index 0d622ab4142ad0..18eb4adae1cfa0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfTKeyTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfTKeyTValueConverter.cs @@ -14,7 +14,19 @@ internal class ImmutableDictionaryOfTKeyTValueConverter)state.Current.ReturnValue!)[key] = value; + Dictionary dictionary = (Dictionary)state.Current.ReturnValue!; + + if (options.AllowDuplicateProperties) + { + dictionary[key] = value; + } + else + { + if (!dictionary.TryAdd(key, value)) + { + ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(); + } + } } internal sealed override bool CanHaveMetadata => false; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs index 78cada33e7ddba..b15e9d86eed559 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization.Metadata; @@ -37,7 +38,20 @@ protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref R internal sealed override bool IsConvertibleCollection => true; protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) { - state.Current.ReturnValue = _mapConstructor((List>)state.Current.ReturnValue!); + List> listToConvert = (List>)state.Current.ReturnValue!; + TMap map = _mapConstructor(listToConvert); + state.Current.ReturnValue = map; + + if (!options.AllowDuplicateProperties) + { + int totalItemsAdded = listToConvert.Count; + int mapCount = ((ICollection>)map).Count; + + if (mapCount != totalItemsAdded) + { + ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(); + } + } } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonArrayConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonArrayConverter.cs index f30821770a1824..dfcb0118c91d56 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonArrayConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonArrayConverter.cs @@ -25,7 +25,9 @@ public override void Write(Utf8JsonWriter writer, JsonArray? value, JsonSerializ switch (reader.TokenType) { case JsonTokenType.StartArray: - return ReadList(ref reader, options.GetNodeOptions()); + return options.AllowDuplicateProperties + ? ReadAsJsonElement(ref reader, options.GetNodeOptions()) + : ReadAsJsonNode(ref reader, options.GetNodeOptions()); case JsonTokenType.Null: return null; default: @@ -34,12 +36,35 @@ public override void Write(Utf8JsonWriter writer, JsonArray? value, JsonSerializ } } - public static JsonArray ReadList(ref Utf8JsonReader reader, JsonNodeOptions? options = null) + internal static JsonArray ReadAsJsonElement(ref Utf8JsonReader reader, JsonNodeOptions options) { JsonElement jElement = JsonElement.ParseValue(ref reader); return new JsonArray(jElement, options); } + internal static JsonArray ReadAsJsonNode(ref Utf8JsonReader reader, JsonNodeOptions options) + { + Debug.Assert(reader.TokenType == JsonTokenType.StartArray); + + JsonArray jArray = new JsonArray(options); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + { + return jArray; + } + + JsonNode? item = JsonNodeConverter.ReadAsJsonNode(ref reader, options); + jArray.Add(item); + } + + // JSON is invalid so reader would have already thrown. + Debug.Fail("End array token not found."); + ThrowHelper.ThrowJsonException(); + return null; + } + internal override JsonSchema? GetSchema(JsonNumberHandling _) => new() { Type = JsonSchemaType.Array }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs index a88039e2a42117..5a69debf09cd7a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Text.Json.Nodes; using System.Text.Json.Schema; -using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization.Converters { @@ -14,15 +13,7 @@ namespace System.Text.Json.Serialization.Converters /// internal sealed class JsonNodeConverter : JsonConverter { - private static JsonNodeConverter? s_nodeConverter; - private static JsonArrayConverter? s_arrayConverter; - private static JsonObjectConverter? s_objectConverter; - private static JsonValueConverter? s_valueConverter; - - public static JsonNodeConverter Instance => s_nodeConverter ??= new JsonNodeConverter(); - public static JsonArrayConverter ArrayConverter => s_arrayConverter ??= new JsonArrayConverter(); - public static JsonObjectConverter ObjectConverter => s_objectConverter ??= new JsonObjectConverter(); - public static JsonValueConverter ValueConverter => s_valueConverter ??= new JsonValueConverter(); + internal static JsonNodeConverter Instance { get; } = new JsonNodeConverter(); public override void Write(Utf8JsonWriter writer, JsonNode? value, JsonSerializerOptions options) { @@ -37,6 +28,34 @@ public override void Write(Utf8JsonWriter writer, JsonNode? value, JsonSerialize } public override JsonNode? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return options.AllowDuplicateProperties + ? ReadAsJsonElement(ref reader, options.GetNodeOptions()) + : ReadAsJsonNode(ref reader, options.GetNodeOptions()); + } + + internal static JsonNode? ReadAsJsonElement(ref Utf8JsonReader reader, JsonNodeOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.String: + case JsonTokenType.False: + case JsonTokenType.True: + case JsonTokenType.Number: + return JsonValueConverter.ReadNonNullPrimitiveValue(ref reader, options); + case JsonTokenType.StartObject: + return JsonObjectConverter.ReadAsJsonElement(ref reader, options); + case JsonTokenType.StartArray: + return JsonArrayConverter.ReadAsJsonElement(ref reader, options); + case JsonTokenType.Null: + return null; + default: + Debug.Assert(false); + throw new JsonException(); + } + } + + internal static JsonNode? ReadAsJsonNode(ref Utf8JsonReader reader, JsonNodeOptions options) { switch (reader.TokenType) { @@ -44,11 +63,11 @@ public override void Write(Utf8JsonWriter writer, JsonNode? value, JsonSerialize case JsonTokenType.False: case JsonTokenType.True: case JsonTokenType.Number: - return ValueConverter.Read(ref reader, typeToConvert, options); + return JsonValueConverter.ReadNonNullPrimitiveValue(ref reader, options); case JsonTokenType.StartObject: - return ObjectConverter.Read(ref reader, typeToConvert, options); + return JsonObjectConverter.ReadAsJsonNode(ref reader, options); case JsonTokenType.StartArray: - return ArrayConverter.Read(ref reader, typeToConvert, options); + return JsonArrayConverter.ReadAsJsonNode(ref reader, options); case JsonTokenType.Null: return null; default: diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverterFactory.cs index cd898eb812d819..5b622da8823af8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverterFactory.cs @@ -9,21 +9,25 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class JsonNodeConverterFactory : JsonConverterFactory { + private static readonly JsonArrayConverter s_arrayConverter = new JsonArrayConverter(); + private static readonly JsonObjectConverter s_objectConverter = new JsonObjectConverter(); + private static readonly JsonValueConverter s_valueConverter = new JsonValueConverter(); + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { if (typeof(JsonValue).IsAssignableFrom(typeToConvert)) { - return JsonNodeConverter.ValueConverter; + return s_valueConverter; } if (typeof(JsonObject) == typeToConvert) { - return JsonNodeConverter.ObjectConverter; + return s_objectConverter; } if (typeof(JsonArray) == typeToConvert) { - return JsonNodeConverter.ArrayConverter; + return s_arrayConverter; } Debug.Assert(typeof(JsonNode) == typeToConvert); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs index 44f81126d7a65d..d94ba3ce6268ec 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs @@ -31,7 +31,20 @@ internal override void ReadElementAndSetProperty( Debug.Assert(value == null || value is JsonNode); JsonNode? jNodeValue = value; - jObject[propertyName] = jNodeValue; + if (options.AllowDuplicateProperties) + { + jObject[propertyName] = jNodeValue; + } + else + { + // TODO: Use TryAdd once https://github.com/dotnet/runtime/issues/110244 is resolved. + if (jObject.ContainsKey(propertyName)) + { + ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(propertyName); + } + + jObject.Add(propertyName, jNodeValue); + } } public override void Write(Utf8JsonWriter writer, JsonObject? value, JsonSerializerOptions options) @@ -50,7 +63,9 @@ public override void Write(Utf8JsonWriter writer, JsonObject? value, JsonSeriali switch (reader.TokenType) { case JsonTokenType.StartObject: - return ReadObject(ref reader, options.GetNodeOptions()); + return options.AllowDuplicateProperties + ? ReadAsJsonElement(ref reader, options.GetNodeOptions()) + : ReadAsJsonNode(ref reader, options.GetNodeOptions()); case JsonTokenType.Null: return null; default: @@ -59,11 +74,44 @@ public override void Write(Utf8JsonWriter writer, JsonObject? value, JsonSeriali } } - public static JsonObject ReadObject(ref Utf8JsonReader reader, JsonNodeOptions? options) + internal static JsonObject ReadAsJsonElement(ref Utf8JsonReader reader, JsonNodeOptions options) { JsonElement jElement = JsonElement.ParseValue(ref reader); - JsonObject jObject = new JsonObject(jElement, options); - return jObject; + return new JsonObject(jElement, options); + } + + internal static JsonObject ReadAsJsonNode(ref Utf8JsonReader reader, JsonNodeOptions options) + { + Debug.Assert(reader.TokenType == JsonTokenType.StartObject); + + JsonObject jObject = new JsonObject(options); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return jObject; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + // JSON is invalid so reader would have already thrown. + Debug.Fail("Property name expected."); + ThrowHelper.ThrowJsonException(); + } + + string propertyName = reader.GetString()!; + reader.Read(); // Move to the value token. + JsonNode? value = JsonNodeConverter.ReadAsJsonNode(ref reader, options); + + // To have parity with the lazy JsonObject, we throw on duplicates. + jObject.Add(propertyName, value); + } + + // JSON is invalid so reader would have already thrown. + Debug.Fail("End object token not found."); + ThrowHelper.ThrowJsonException(); + return null; } internal override JsonSchema? GetSchema(JsonNumberHandling _) => new() { Type = JsonSchemaType.Object }; 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 b912ed898b42bd..bf3e26482a4413 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 @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Text.Json.Nodes; using System.Text.Json.Schema; -using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization.Converters { @@ -27,8 +27,39 @@ public override void Write(Utf8JsonWriter writer, JsonValue? value, JsonSerializ return null; } - JsonElement element = JsonElement.ParseValue(ref reader); - return JsonValue.CreateFromElement(ref element, options.GetNodeOptions()); + switch (reader.TokenType) + { + case JsonTokenType.String: + case JsonTokenType.False: + case JsonTokenType.True: + case JsonTokenType.Number: + return ReadNonNullPrimitiveValue(ref reader, options.GetNodeOptions()); + default: + JsonElement element = JsonElement.ParseValue(ref reader, options.AllowDuplicateProperties); + return JsonValue.CreateFromElement(ref element, options.GetNodeOptions()); + } + } + + 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(); + } } internal override JsonSchema? GetSchema(JsonNumberHandling _) => JsonSchema.CreateTrueSchema(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs index 203e3e9b0b1ccd..b650e92dbb2f39 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs @@ -98,7 +98,7 @@ public DefaultObjectConverter() { if (options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonElement) { - return JsonElement.ParseValue(ref reader); + return JsonElement.ParseValue(ref reader, options.AllowDuplicateProperties); } Debug.Assert(options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonNode); @@ -111,7 +111,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, if (options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonElement) { - JsonElement element = JsonElement.ParseValue(ref reader); + JsonElement element = JsonElement.ParseValue(ref reader, options.AllowDuplicateProperties); // Edge case where we want to lookup for a reference when parsing into typeof(object) if (options.ReferenceHandlingStrategy == JsonKnownReferenceHandler.Preserve && diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs index 0b9bae378cf627..c2ac27a8c4e2af 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs @@ -134,7 +134,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, state.Current.ReturnValue = obj; state.Current.ObjectState = StackFrameObjectState.CreatedObject; - state.Current.InitializeRequiredPropertiesValidationState(jsonTypeInfo); + state.Current.InitializePropertiesValidationState(jsonTypeInfo); } else { @@ -271,7 +271,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, internal static void PopulatePropertiesFastPath(object obj, JsonTypeInfo jsonTypeInfo, JsonSerializerOptions options, ref Utf8JsonReader reader, scoped ref ReadStack state) { jsonTypeInfo.OnDeserializing?.Invoke(obj); - state.Current.InitializeRequiredPropertiesValidationState(jsonTypeInfo); + state.Current.InitializePropertiesValidationState(jsonTypeInfo); // Process all properties. while (true) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs index 849c8fdf4686b4..c6de4fba0e1a0b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs @@ -168,7 +168,7 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo jsonTypeInfo.OnDeserializing?.Invoke(populatedObject); state.Current.ObjectState = StackFrameObjectState.CreatedObject; - state.Current.InitializeRequiredPropertiesValidationState(jsonTypeInfo); + state.Current.InitializePropertiesValidationState(jsonTypeInfo); return base.OnTryRead(ref reader, typeToConvert, options, ref state, out value); } @@ -240,11 +240,27 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo if (extDictionary is IDictionary dict) { - dict[dataExtKey] = (JsonElement)propValue!; + if (options.AllowDuplicateProperties) + { + dict[dataExtKey] = (JsonElement)propValue!; + } + else if (!dict.TryAdd(dataExtKey, (JsonElement)propValue!)) + { + ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(dataExtKey); + } } else { - ((IDictionary)extDictionary)[dataExtKey] = propValue!; + IDictionary objDict = (IDictionary)extDictionary; + + if (options.AllowDuplicateProperties) + { + objDict[dataExtKey] = propValue!; + } + else if (!objDict.TryAdd(dataExtKey, propValue!)) + { + ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(dataExtKey); + } } } } @@ -581,7 +597,7 @@ private void BeginRead(scoped ref ReadStack state, JsonSerializerOptions options ThrowHelper.ThrowInvalidOperationException_ConstructorParameterIncompleteBinding(Type); } - state.Current.InitializeRequiredPropertiesValidationState(jsonTypeInfo); + state.Current.InitializePropertiesValidationState(jsonTypeInfo); // Set current JsonPropertyInfo to null to avoid conflicts on push. state.Current.JsonPropertyInfo = null; @@ -613,7 +629,10 @@ protected static bool TryLookupConstructorParameter( createExtensionProperty: false); // Mark the property as read from the payload if required. - state.Current.MarkRequiredPropertyAsRead(jsonPropertyInfo); + if (!useExtensionProperty) + { + state.Current.MarkPropertyAsRead(jsonPropertyInfo); + } jsonParameterInfo = jsonPropertyInfo.AssociatedParameter; if (jsonParameterInfo != null) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonDocumentConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonDocumentConverter.cs index 9fbb293639152c..8c0f44b681a515 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonDocumentConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonDocumentConverter.cs @@ -10,10 +10,8 @@ internal sealed class JsonDocumentConverter : JsonConverter { public override bool HandleNull => true; - public override JsonDocument Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return JsonDocument.ParseValue(ref reader); - } + public override JsonDocument Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + JsonDocument.ParseValue(ref reader, options.AllowDuplicateProperties); public override void Write(Utf8JsonWriter writer, JsonDocument? value, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs index 718d9fa8024630..b959e881e62113 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs @@ -10,7 +10,7 @@ internal sealed class JsonElementConverter : JsonConverter { public override JsonElement Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return JsonElement.ParseValue(ref reader); + return JsonElement.ParseValue(ref reader, options.AllowDuplicateProperties); } public override void Write(Utf8JsonWriter writer, JsonElement value, JsonSerializerOptions options) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs index b96decb395ae7a..8fda42bee9345f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs @@ -512,6 +512,7 @@ public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right) left._indentCharacter == right._indentCharacter && left._indentSize == right._indentSize && left._typeInfoResolver == right._typeInfoResolver && + left._allowDuplicateProperties == right._allowDuplicateProperties && CompareLists(left._converters, right._converters); static bool CompareLists(ConfigurationList? left, ConfigurationList? right) @@ -572,6 +573,7 @@ public int GetHashCode(JsonSerializerOptions options) AddHashCode(ref hc, options._indentCharacter); AddHashCode(ref hc, options._indentSize); AddHashCode(ref hc, options._typeInfoResolver); + AddHashCode(ref hc, options._allowDuplicateProperties); AddListHashCode(ref hc, options._converters); return hc.ToHashCode(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 27dcd9b34cb009..01e426eb1aaaea 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -97,6 +97,7 @@ public static JsonSerializerOptions Web private bool _writeIndented; private char _indentCharacter = JsonConstants.DefaultIndentCharacter; private int _indentSize = JsonConstants.DefaultIndentSize; + private bool _allowDuplicateProperties = true; /// /// Constructs a new instance. @@ -149,6 +150,7 @@ public JsonSerializerOptions(JsonSerializerOptions options) _writeIndented = options._writeIndented; _indentCharacter = options._indentCharacter; _indentSize = options._indentSize; + _allowDuplicateProperties = options._allowDuplicateProperties; _typeInfoResolver = options._typeInfoResolver; EffectiveMaxDepth = options.EffectiveMaxDepth; ReferenceHandlingStrategy = options.ReferenceHandlingStrategy; @@ -833,6 +835,31 @@ public bool RespectRequiredConstructorParameters } } + /// + /// Defines whether duplicate property names are allowed when deserializing JSON objects. + /// + /// + /// Thrown if this property is set after serialization or deserialization has occurred. + /// + /// + /// + /// By default, it's set to true. If set to false, is thrown + /// when a duplicate property name is encountered during deserialization. + /// + /// + /// Duplicate property names are not allowed in serialization. + /// + /// + public bool AllowDuplicateProperties + { + get => _allowDuplicateProperties; + set + { + VerifyMutable(); + _allowDuplicateProperties = value; + } + } + /// /// Returns true if options uses compatible built-in resolvers or a combination of compatible built-in resolvers. /// @@ -1020,9 +1047,10 @@ internal JsonDocumentOptions GetDocumentOptions() { return new JsonDocumentOptions { + AllowDuplicateProperties = AllowDuplicateProperties, AllowTrailingCommas = AllowTrailingCommas, CommentHandling = ReadCommentHandling, - MaxDepth = MaxDepth + MaxDepth = MaxDepth, }; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index 2dcf18a91cfae8..cacd6f84990ef4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs @@ -854,20 +854,20 @@ internal bool ReadJsonAndAddExtensionProperty( if (reader.TokenType == JsonTokenType.Null) { // A null JSON value is treated as a null object reference. - dictionaryObjectValue[state.Current.JsonPropertyNameAsString!] = null; + AddProperty(in state.Current, dictionaryObjectValue, null); } else { JsonConverter converter = GetDictionaryValueConverter(); object value = converter.Read(ref reader, JsonTypeInfo.ObjectType, Options)!; - dictionaryObjectValue[state.Current.JsonPropertyNameAsString!] = value; + AddProperty(in state.Current, dictionaryObjectValue, value); } } else if (propValue is IDictionary dictionaryElementValue) { JsonConverter converter = GetDictionaryValueConverter(); JsonElement value = converter.Read(ref reader, typeof(JsonElement), Options); - dictionaryElementValue[state.Current.JsonPropertyNameAsString!] = value; + AddProperty(in state.Current, dictionaryElementValue, value); } else { @@ -890,6 +890,30 @@ JsonConverter GetDictionaryValueConverter() Debug.Assert(dictionaryValueInfo is JsonTypeInfo); return ((JsonTypeInfo)dictionaryValueInfo).EffectiveConverter; } + + void AddProperty(ref readonly ReadStackFrame current, IDictionary d, TValue value) + { + string property = current.JsonPropertyNameAsString!; + if (Options.AllowDuplicateProperties) + { + d[property] = value; + } + else + { +#if NET + if (!d.TryAdd(property, value)) +#else + if (d.ContainsKey(property)) +#endif + { + ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(current.JsonPropertyInfo!); + } + +#if !NET + d[property] = value; +#endif + } + } } internal abstract bool ReadJsonAndSetMember(object obj, scoped ref ReadStack state, ref Utf8JsonReader reader); @@ -1043,28 +1067,27 @@ public JsonNumberHandling? NumberHandling internal abstract object? DefaultValue { get; } /// - /// Required property index on the list of JsonTypeInfo properties. - /// It is used as a unique identifier for required properties. + /// Property index on the list of JsonTypeInfo properties. + /// It is used as a unique identifier for properties. /// It is set just before property is configured and does not change afterward. /// It is not equivalent to index on the properties list /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - internal int RequiredPropertyIndex + internal int PropertyIndex { get { Debug.Assert(IsConfigured); - Debug.Assert(IsRequired); - return _index; + return _propertyIndex; } set { Debug.Assert(!IsConfigured); - _index = value; + _propertyIndex = value; } } - private int _index; + private int _propertyIndex; internal bool IsOverriddenOrShadowedBy(JsonPropertyInfo other) => MemberName == other.MemberName && DeclaringType.IsAssignableFrom(other.DeclaringType); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs index ed293cc4cb9ff2..de34a5bb0b7fd5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs @@ -303,7 +303,7 @@ internal override bool ReadJsonAndSetMember(object obj, scoped ref ReadStack sta } success = true; - state.Current.MarkRequiredPropertyAsRead(this); + state.Current.MarkPropertyAsRead(this); } else if (EffectiveConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { @@ -325,7 +325,7 @@ internal override bool ReadJsonAndSetMember(object obj, scoped ref ReadStack sta } success = true; - state.Current.MarkRequiredPropertyAsRead(this); + state.Current.MarkPropertyAsRead(this); } else { @@ -354,7 +354,7 @@ internal override bool ReadJsonAndSetMember(object obj, scoped ref ReadStack sta } } - state.Current.MarkRequiredPropertyAsRead(this); + state.Current.MarkPropertyAsRead(this); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index a11d3634aa623b..c6485d6863b18a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; @@ -30,9 +31,10 @@ public abstract partial class JsonTypeInfo internal delegate T ParameterizedConstructorDelegate(TArg0? arg0, TArg1? arg1, TArg2? arg2, TArg3? arg3); /// - /// Indices of required properties. + /// Negated bitmask of the required properties, indexed by . /// - internal int NumberOfRequiredProperties { get; private set; } + internal BitArray? OptionalPropertiesMask { get; private set; } + internal bool ShouldTrackRequiredProperties => OptionalPropertiesMask is not null; private Action? _onSerializing; private Action? _onSerialized; @@ -1072,12 +1074,13 @@ internal void ConfigureProperties() Dictionary propertyIndex = new(properties.Count, comparer); List propertyCache = new(properties.Count); - int numberOfRequiredProperties = 0; bool arePropertiesSorted = true; int previousPropertyOrder = int.MinValue; + BitArray? requiredPropertiesMask = null; - foreach (JsonPropertyInfo property in properties) + for (int i = 0; i < properties.Count; i++) { + JsonPropertyInfo property = properties[i]; Debug.Assert(property.DeclaringTypeInfo == this); if (property.IsExtensionData) @@ -1096,9 +1099,11 @@ internal void ConfigureProperties() } else { + property.PropertyIndex = i; + if (property.IsRequired) { - property.RequiredPropertyIndex = numberOfRequiredProperties++; + (requiredPropertiesMask ??= new BitArray(properties.Count))[i] = true; } if (arePropertiesSorted) @@ -1125,7 +1130,7 @@ internal void ConfigureProperties() propertyCache.StableSortByKey(static propInfo => propInfo.Order); } - NumberOfRequiredProperties = numberOfRequiredProperties; + OptionalPropertiesMask = requiredPropertiesMask?.Not(); _propertyCache = propertyCache.ToArray(); _propertyIndex = propertyIndex; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs index 0c973ea2b2350c..d6010c57f98212 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs @@ -75,12 +75,12 @@ public JsonTypeInfo BaseJsonTypeInfo // Whether to use custom number handling. public JsonNumberHandling? NumberHandling; - // Represents required properties which have value assigned. - // Each bit corresponds to a required property. + // Represents known (non-extension) properties which have value assigned. + // Each bit corresponds to a property. // False means that property is not set (not yet occurred in the payload). - // Length of the BitArray is equal to number of required properties. - // Every required JsonPropertyInfo has RequiredPropertyIndex property which maps to an index in this BitArray. - public BitArray? RequiredPropertiesSet; + // Length of the BitArray is equal to number of non-extension properties. + // Every JsonPropertyInfo has PropertyIndex property which maps to an index in this BitArray. + public BitArray? AssignedProperties; // Tracks state related to property population. public bool HasParentObject; @@ -128,38 +128,52 @@ public bool IsProcessingEnumerable() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void MarkRequiredPropertyAsRead(JsonPropertyInfo propertyInfo) + public void MarkPropertyAsRead(JsonPropertyInfo propertyInfo) { - if (propertyInfo.IsRequired) + if (AssignedProperties is { }) { - Debug.Assert(RequiredPropertiesSet != null); - RequiredPropertiesSet[propertyInfo.RequiredPropertyIndex] = true; + if (!propertyInfo.Options.AllowDuplicateProperties) + { + if (AssignedProperties[propertyInfo.PropertyIndex]) + { + ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(propertyInfo); + } + } + + AssignedProperties[propertyInfo.PropertyIndex] = true; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void InitializeRequiredPropertiesValidationState(JsonTypeInfo typeInfo) + internal void InitializePropertiesValidationState(JsonTypeInfo typeInfo) { - Debug.Assert(RequiredPropertiesSet == null); + Debug.Assert(AssignedProperties is null); - if (typeInfo.NumberOfRequiredProperties > 0) + if (typeInfo.ShouldTrackRequiredProperties || typeInfo.Options.AllowDuplicateProperties is false) { - RequiredPropertiesSet = new BitArray(typeInfo.NumberOfRequiredProperties); + // This may be slightly larger than required (e.g. if there's an extension property) + AssignedProperties = new BitArray(typeInfo.Properties.Count); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void ValidateAllRequiredPropertiesAreRead(JsonTypeInfo typeInfo) { - if (typeInfo.NumberOfRequiredProperties > 0) + if (typeInfo.ShouldTrackRequiredProperties) { - Debug.Assert(RequiredPropertiesSet != null); + Debug.Assert(AssignedProperties is not null); + Debug.Assert(typeInfo.OptionalPropertiesMask is not null); - if (!RequiredPropertiesSet.HasAllSet()) + // All properties must be either assigned or optional + BitArray assignedOrNotRequiredPropertiesSet = AssignedProperties.Or(typeInfo.OptionalPropertiesMask); + + if (!assignedOrNotRequiredPropertiesSet.HasAllSet()) { - ThrowHelper.ThrowJsonException_JsonRequiredPropertyMissing(typeInfo, RequiredPropertiesSet); + ThrowHelper.ThrowJsonException_JsonRequiredPropertyMissing(typeInfo, assignedOrNotRequiredPropertiesSet); } } + + AssignedProperties = null; } [DebuggerBrowsable(DebuggerBrowsableState.Never)] diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index b93651e0cddb6f..bc812b96fadff9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -288,7 +288,7 @@ public static void ThrowInvalidOperationException_JsonPropertyRequiredAndExtensi } [DoesNotReturn] - public static void ThrowJsonException_JsonRequiredPropertyMissing(JsonTypeInfo parent, BitArray requiredPropertiesSet) + public static void ThrowJsonException_JsonRequiredPropertyMissing(JsonTypeInfo parent, BitArray assignedOrNotRequiredPropertiesSet) { StringBuilder listOfMissingPropertiesBuilder = new(); bool first = true; @@ -298,7 +298,7 @@ public static void ThrowJsonException_JsonRequiredPropertyMissing(JsonTypeInfo p foreach (JsonPropertyInfo property in parent.PropertyCache) { - if (!property.IsRequired || requiredPropertiesSet[property.RequiredPropertyIndex]) + if (assignedOrNotRequiredPropertiesSet[property.PropertyIndex]) { continue; } @@ -323,6 +323,46 @@ public static void ThrowJsonException_JsonRequiredPropertyMissing(JsonTypeInfo p throw new JsonException(SR.Format(SR.JsonRequiredPropertiesMissing, parent.Type, listOfMissingPropertiesBuilder.ToString())); } + [DoesNotReturn] + public static void ThrowJsonException_DuplicatePropertyNotAllowed(JsonPropertyInfo property) + { + throw new JsonException(SR.Format(SR.DuplicatePropertiesNotAllowed_JsonPropertyInfo, property.Name, property.DeclaringType)); + } + + [DoesNotReturn] + public static void ThrowJsonException_DuplicatePropertyNotAllowed() + { + throw new JsonException(SR.Format(SR.DuplicatePropertiesNotAllowed)); + } + + [DoesNotReturn] + public static void ThrowJsonException_DuplicatePropertyNotAllowed(string name) + { + throw new JsonException(SR.Format(SR.DuplicatePropertiesNotAllowed_NameSpan, Truncate(name))); + } + + [DoesNotReturn] + public static void ThrowJsonException_DuplicatePropertyNotAllowed(ReadOnlySpan nameBytes) + { + string name = JsonHelpers.Utf8GetString(nameBytes); + throw new JsonException(SR.Format(SR.DuplicatePropertiesNotAllowed_NameSpan, Truncate(name))); + } + + private static string Truncate(ReadOnlySpan str) + { + const int MaxLength = 15; + + if (str.Length <= MaxLength) + { + return str.ToString(); + } + + Span builder = stackalloc char[MaxLength + 3]; + str.Slice(0, MaxLength).CopyTo(builder); + builder[MaxLength] = builder[MaxLength + 1] = builder[MaxLength + 2] = '.'; + return builder.ToString(); + } + [DoesNotReturn] public static void ThrowInvalidOperationException_NamingPolicyReturnNull(JsonNamingPolicy namingPolicy) { diff --git a/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.cs b/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.cs index 96ae1b0803c89c..dcd7707e958f08 100644 --- a/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.cs +++ b/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.Specialized; +using System.Linq; using System.Text.Encodings.Web; using System.Threading.Tasks; using Xunit; @@ -1077,6 +1078,24 @@ public async Task DeserializeDictionaryWithDuplicateProperties() Assert.Equal("e", foo.DictProperty["c"]); } + [Fact] + public async Task DeserializeDictionaryWithDuplicatePropertiesThrow() + { + JsonSerializerOptions options = JsonTestSerializerOptions.DisallowDuplicateProperties; + + Exception ex = await Assert.ThrowsAsync(() => + Serializer.DeserializeWrapper(@"{""BoolProperty"": false, ""BoolProperty"": true}", options)); + Assert.Contains("Duplicate", ex.Message); + + ex = await Assert.ThrowsAsync(() => + Serializer.DeserializeWrapper(@"{""BoolProperty"": false, ""IntProperty"" : 1, ""BoolProperty"": true , ""IntProperty"" : 2}", options)); + Assert.Contains("Duplicate", ex.Message); + + ex = await Assert.ThrowsAsync(() => + Serializer.DeserializeWrapper(@"{""DictProperty"" : {""a"" : ""b"", ""c"" : ""d""},""DictProperty"" : {""b"" : ""b"", ""c"" : ""e""}}", options)); + Assert.Contains("Duplicate", ex.Message); + } + public class PocoDuplicate { public bool BoolProperty { get; set; } @@ -1084,6 +1103,56 @@ public class PocoDuplicate public Dictionary DictProperty { get; set; } } + [Fact] + public async Task DeserializeNestedDictionaryWithDuplicatePropertiesThrow() + { + JsonSerializerOptions options = JsonTestSerializerOptions.DisallowDuplicateProperties; + + Exception ex = await Assert.ThrowsAsync(() => + Serializer.DeserializeWrapper("""{"key": {"A": "a", "A": "a"}}""", options)); + Assert.Contains("Duplicate", ex.Message); + + Assert.Equal("a", (await Serializer.DeserializeWrapper("""{"key": {"A": "a", "A": "a"}}""")).key["A"]); + } + + public static IEnumerable TestDictionaryTypes = + CollectionTestTypes.DeserializableNonGenericDictionaryTypes() + .Concat(CollectionTestTypes.DeserializableDictionaryTypes()) + .Concat(CollectionTestTypes.DeserializableDictionaryTypes()) + .Select(x => [x]); + + [Theory] + [InlineData(typeof(ImmutableSortedDictionary))] + [InlineData(typeof(DictionaryThatOnlyImplementsIDictionaryOfStringTValue))] + [MemberData(nameof(TestDictionaryTypes))] + public async Task DeserializeBuiltInDictionaryWithDuplicatePropertiesThrow(Type t) + { + JsonSerializerOptions options = JsonTestSerializerOptions.DisallowDuplicateProperties; + + Exception ex = await Assert.ThrowsAsync(() => + Serializer.DeserializeWrapper("""{"1": "a", "1": "a"}""", t, options)); + Assert.Contains("Duplicate", ex.Message); + + // Assert no throw + _ = await Serializer.DeserializeWrapper("""{"1": "a", "1": "a"}""", t); + } + + public static IEnumerable TestStringKeyDictionaryTypes = + CollectionTestTypes.DeserializableNonGenericDictionaryTypes() + .Concat(CollectionTestTypes.DeserializableDictionaryTypes()) + .Select(x => [x]); + + [Theory] + [InlineData(typeof(ImmutableSortedDictionary))] + [InlineData(typeof(DictionaryThatOnlyImplementsIDictionaryOfStringTValue))] + [MemberData(nameof(TestStringKeyDictionaryTypes))] + public async Task DeserializeCaseInsensitiveBuiltInDictionaryWithDuplicatePropertiesNoThrow(Type t) + { + JsonSerializerOptions options = JsonTestSerializerOptions.DisallowDuplicatePropertiesIgnoringCase; + + await Serializer.DeserializeWrapper("""{"a": "a", "A": "a"}""", t, options); // Assert no throw + } + public class ClassWithPopulatedDictionaryAndNoSetter { public ClassWithPopulatedDictionaryAndNoSetter() diff --git a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs index d46253c65761e1..86d8119afb121a 100644 --- a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs +++ b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs @@ -1695,5 +1695,131 @@ public ClassWithConflictingCaseInsensitiveProperties(string from, string to) To = to; } } + + [Fact] + public async Task RespectAllowDuplicateProp_DuplicateCtorParam() + { + string json = """{"X":1,"Y":2,"X":3}"""; + + Exception ex = await Assert.ThrowsAsync( + () => Serializer.DeserializeWrapper(json, JsonTestSerializerOptions.DisallowDuplicateProperties)); + Assert.Contains("Duplicate", ex.Message); + + Assert.Equal(3, (await Serializer.DeserializeWrapper(json)).X); + } + + [Theory] + [InlineData("""{"X":1,"X":2}""", typeof(Point_2D))] + [InlineData("""{"Y":1,"Y":2}""", typeof(Point_2D))] + [InlineData("""{"X":1,"Y":2,"X":3}""", typeof(Point_2D))] + [InlineData("""{"X":1,"Y":2,"Y":3}""", typeof(Point_2D))] + [InlineData("""{"Y":1,"X":2,"X":3}""", typeof(Point_2D))] + [InlineData("""{"Y":1,"X":2,"Y":3}""", typeof(Point_2D))] + [InlineData("""{"X":1,"Y":2,"X":3}""", typeof(Point_3D))] + [InlineData("""{"X":1,"Y":2,"Z":3,"X":4}""", typeof(Class_ExtraProperty_ExtData))] + [InlineData("""{"X":1,"Y":2,"Z":3,"Y":4}""", typeof(Class_ExtraProperty_ExtData))] + [InlineData("""{"X":1,"Y":2,"Z":3,"Z":4}""", typeof(Class_ExtraProperty_ExtData))] + [InlineData("""{"X":1,"Y":2,"Z":3,"X":4}""", typeof(Class_ExtraProperty_JsonElementDictionaryExtData))] + [InlineData("""{"X":1,"Y":2,"Z":3,"Y":4}""", typeof(Class_ExtraProperty_JsonElementDictionaryExtData))] + [InlineData("""{"X":1,"Y":2,"Z":3,"Z":4}""", typeof(Class_ExtraProperty_JsonElementDictionaryExtData))] + public async Task RespectAllowDuplicateProp_Small(string json, Type type) + { + Exception ex = await Assert.ThrowsAsync( + () => Serializer.DeserializeWrapper(json, type, JsonTestSerializerOptions.DisallowDuplicateProperties)); + Assert.Contains("Duplicate", ex.Message); + + await Serializer.DeserializeWrapper(json, type); // Assert no throw + + ex = await Assert.ThrowsAsync( + () => Serializer.DeserializeWrapper(json, type, JsonTestSerializerOptions.DisallowDuplicateProperties)); + Assert.Contains("Duplicate", ex.Message); + + await Serializer.DeserializeWrapper(json, type); // Assert no throw + } + + [Theory] + [InlineData("""{"X":1,"Y":2,"Z":3,"x":4}""", true)] + [InlineData("""{"X":1,"Y":2,"Z":3,"y":4}""", true)] + [InlineData("""{"X":1,"Y":2,"Z":3,"z":4}""", false)] // Dictionary extension properties are case-sensitive + public async Task RespectAllowDuplicatePropCaseInsensitive_Small(string json, bool throws) + { + JsonSerializerOptions options = JsonTestSerializerOptions.DisallowDuplicatePropertiesIgnoringCase; + + if (throws) + { + Exception ex = await Assert.ThrowsAsync( + () => Serializer.DeserializeWrapper(json, options)); + Assert.Contains("Duplicate", ex.Message); + } + else + { + await Serializer.DeserializeWrapper(json, options); // Assert no throw + } + } + + public class Class_ExtraProperty_ExtData + { + public int X { get; set; } + + public int Y { get; set; } + + [JsonExtensionData] + public Dictionary ExtensionData { get; set; } + + public Class_ExtraProperty_ExtData(int x) + { + X = x; + } + } + + public class Class_ExtraProperty_JsonElementDictionaryExtData + { + public int X { get; set; } + + public int Y { get; set; } + + [JsonExtensionData] + public Dictionary ExtensionData { get; set; } + + public Class_ExtraProperty_JsonElementDictionaryExtData(int x) + { + X = x; + } + } + + [Theory] + [InlineData("P0")] + [InlineData("Prop")] + [InlineData("ExtensionDataProp")] + public async Task RespectAllowDuplicateProp_Large(string duplicatedProperty) + { + string json = $$""" + { + "P0": 0, + "P1": 1, + "P2": 2, + "P3": 3, + "P4": 4, + "P5": 5, + "Prop": 6, + "ExtensionDataProp": 7, + "{{duplicatedProperty}}": 42 + } + """; + + Exception ex = await Assert.ThrowsAsync( + () => Serializer.DeserializeWrapper(json, JsonTestSerializerOptions.DisallowDuplicateProperties)); + Assert.Contains("Duplicate", ex.Message); + + await Serializer.DeserializeWrapper(json); // Assert no throw + } + + public record class Class_ManyParameters_ExtraProperty_ExtData(int P0, int P1, int P2, int P3, int P4, int P5) + { + public int Prop { get; set; } + + [JsonExtensionData] + public Dictionary ExtensionData { get; set; } + } } } diff --git a/src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs b/src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs index 02f1cea1ceb6d3..5d86c5d54bad17 100644 --- a/src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs @@ -614,6 +614,56 @@ public async Task ExtensionPropertyDuplicateNames(JsonSerializerOptions options) Assert.Null(obj.MyOverflow); } + [Theory] + [InlineData("""{ "1": 0 , "1": 1 }""")] + [InlineData("""{ "1": null, "1": null }""")] + [InlineData("""{ "1": "a" , "1": null }""")] + [InlineData("""{ "1": null, "1": "b" }""")] + public async Task ExtensionProperty_DuplicatesThrow(string payload) + { + JsonSerializerOptions options = JsonTestSerializerOptions.DisallowDuplicateProperties; + + Exception ex = await Assert.ThrowsAsync( + () => Serializer.DeserializeWrapper(payload, options)); + Assert.Contains("Duplicate", ex.Message); + + await Serializer.DeserializeWrapper(payload); // Assert no throw + + ex = await Assert.ThrowsAsync( + () => Serializer.DeserializeWrapper(payload, options)); + Assert.Contains("Duplicate", ex.Message); + + await Serializer.DeserializeWrapper(payload); // Assert no throw + + ex = await Assert.ThrowsAsync( + () => Serializer.DeserializeWrapper(payload, options)); + Assert.Contains("Duplicate", ex.Message); + + await Serializer.DeserializeWrapper(payload); // Assert no throw + } + + [Theory] + [InlineData("""{ "a": 0 , "A": 1 }""")] + [InlineData("""{ "a": null, "A": null }""")] + [InlineData("""{ "a": "a" , "A": null }""")] + [InlineData("""{ "a": null, "A": "b" }""")] + public async Task ExtensionProperty_CaseInsensitiveDuplicatesNoThrow(string payload) + { + JsonSerializerOptions options = JsonTestSerializerOptions.DisallowDuplicatePropertiesIgnoringCase; + + // Dictionary extension properties are always case-sensitive + ICollection d; + d = (await Serializer.DeserializeWrapper(payload, options)).MyOverflow; + Assert.Equal(2, d.Count); + + d = (await Serializer.DeserializeWrapper(payload, options)).MyOverflow; + Assert.Equal(2, d.Count); + + // But JsonObject abides by options + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(payload, options)); + Assert.Contains("Duplicate", ex.Message); + } + [Theory] [MemberData(nameof(JsonSerializerOptions))] public async Task Null_SystemObject(JsonSerializerOptions options) diff --git a/src/libraries/System.Text.Json/tests/Common/JsonTestSerializerOptions.cs b/src/libraries/System.Text.Json/tests/Common/JsonTestSerializerOptions.cs new file mode 100644 index 00000000000000..61d8d927c0aeb6 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Common/JsonTestSerializerOptions.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json +{ + internal static class JsonTestSerializerOptions + { + internal static JsonSerializerOptions DisallowDuplicateProperties => + field ??= new() { AllowDuplicateProperties = false }; + + internal static JsonSerializerOptions DisallowDuplicatePropertiesIgnoringCase => + field ??= new() { AllowDuplicateProperties = false, PropertyNameCaseInsensitive = true }; + } +} diff --git a/src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs b/src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs index df1755fd393d2d..c35822b41d22b2 100644 --- a/src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs @@ -529,5 +529,54 @@ public class ClassWithIgnoredCaseInsensitiveConflict [JsonIgnore] public string Name { get; set; } } + + [Fact] + public async Task DuplicatesIgnoreCase() + { + string json = """{ "myint32":1, "MYINT32":2 }"""; + + JsonSerializerOptions options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + Assert.Equal(2, (await Serializer.DeserializeWrapper(json, options)).MyInt32); + + options = JsonTestSerializerOptions.DisallowDuplicatePropertiesIgnoringCase; + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, options)); + Assert.Contains("Duplicate", ex.Message); + } + + [Fact] + public async Task DuplicatesByKebabCaseNamingPolicy() + { + string json = """{ "my-int32":1, "my-int32":2 }"""; + + JsonSerializerOptions options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower }; + Assert.Equal(2, (await Serializer.DeserializeWrapper(json, options)).MyInt32); + + options = new JsonSerializerOptions { AllowDuplicateProperties = false, PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower }; + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, options)); + Assert.Contains("Duplicate", ex.Message); + } + + [Fact] + public async Task DuplicatesByKebabCaseNamingPolicyIgnoreCase() + { + string json = """{ "my-int32":1, "MY-int32":2 }"""; + + JsonSerializerOptions options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, + PropertyNameCaseInsensitive = true, + }; + + Assert.Equal(2, (await Serializer.DeserializeWrapper(json, options)).MyInt32); + + options = new JsonSerializerOptions { + AllowDuplicateProperties = false, + PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, + PropertyNameCaseInsensitive = true, + }; + + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, options)); + Assert.Contains("Duplicate", ex.Message); + } } } diff --git a/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.Deserialize.cs b/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.Deserialize.cs index b40e30f5e2e267..ef4bff0e34ac16 100644 --- a/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.Deserialize.cs +++ b/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.Deserialize.cs @@ -1666,5 +1666,32 @@ public LinkedList(T value, LinkedList? next) public T? PropertyWithSetter { get; set; } } + + [Fact] + public async Task DuplicateNonReferenceProperties_Preserve() + { + string json = @"{ + ""$id"": ""1"", + ""Name"": ""Angela"", + ""Manager"": { + ""$ref"": ""1"" + }, + ""Name"": ""Angela"" + }"; + + // Baseline + JsonSerializerOptions options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve }; + Employee angela = await Serializer.DeserializeWrapper(json, options); + Assert.Same(angela, angela.Manager); + + options = new JsonSerializerOptions + { + ReferenceHandler = ReferenceHandler.Preserve, + AllowDuplicateProperties = false + }; + + JsonException ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, options)); + Assert.Contains("Duplicate", ex.Message); + } } } diff --git a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestData.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestData.cs index 2ee8e4f4ea0773..1228d1171a1c03 100644 --- a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestData.cs +++ b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestData.cs @@ -100,5 +100,45 @@ public static IEnumerable WriteSuccessCases yield return new object[] { new ClassWithComplexObjects() }; } } + + public static IEnumerable DuplicatePropertyJsonPayloads => field ??= + [ + [$$"""{"p0":0,"p0":42}"""], + [$$"""{"p0":0,"p1":1,"p1":42}"""], + [$$"""{"p0":0,"p1":1,"p2":2,"p2":42}"""], + [$$"""{"p0":0,"p1":1,"p2":2,"p3":3,"p3":42}"""], + [$$"""{"p0":0,"p1":1,"p2":2,"p3":3,"p4":4,"p4":42}"""], + [$$"""{"p0":0,"p1":1,"p2":2,"p3":3,"p4":4,"p5":5,"p5":42}"""], + [$$"""{"p0":0,"p1":1,"p2":2,"p3":3,"p4":4,"p5":5,"p6":6,"p6":42}"""], + + [$$"""{"p0":0,"p1":1,"p0":42}"""], + [$$"""{"p0":0,"p1":1,"p2":2,"p3":3,"p4":4,"p5":5,"p6":6,"p0":42}"""], + + // First occurrence escaped + [$$"""{"p0":0,"p\u0031":1,"p1":42}"""], + [$$"""{"p0":0,"p1":1,"p2":2,"p3":3,"p4":4,"p5":5,"p\u0036":6,"p6":42}"""], + [$$"""{"p\u0030":0,"p1":1,"p0":42}"""], + [$$"""{"p\u0030":0,"p1":1,"p2":2,"p3":3,"p4":4,"p5":5,"p6":6,"p0":42}"""], + + // Last occurrence escaped + [$$"""{"p0":0,"p1":1,"p\u0031":42}"""], + [$$"""{"p0":0,"p1":1,"p2":2,"p3":3,"p4":4,"p5":5,"p6":6,"p\u0036":42}"""], + [$$"""{"p0":0,"p1":1,"p\u0030":42}"""], + [$$"""{"p0":0,"p1":1,"p2":2,"p3":3,"p4":4,"p5":5,"p6":6,"p\u0030":42}"""], + + // Both occurrences escaped + [$$"""{"p0":0,"p\u0031":1,"p\u0031":42}"""], + [$$"""{"p0":0,"p1":1,"p2":2,"p3":3,"p4":4,"p5":5,"p\u0036":6,"p\u0036":42}"""], + [$$"""{"p\u0030":0,"p1":1,"p\u0030":42}"""], + [$$"""{"p\u0030":0,"p1":1,"p2":2,"p3":3,"p4":4,"p5":5,"p6":6,"p\u0030":42}"""], + + [$$"""{"A":[],"A":1}"""], + [$$"""{"A":{"A":1},"A":1}"""], + [$$"""{"A":{"B":1},"A":1}"""], + + // No error + [$$"""{"A":{"A":1} }""", true], + [$$"""{"A":{"B":1},"B":1}""", true], + ]; } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/CollectionTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/CollectionTests.fs index af4326d9ccb4bb..5f54953ac15855 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/CollectionTests.fs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/CollectionTests.fs @@ -175,3 +175,14 @@ let ``Map async deserialization should be supported``() = async { Assert.Equal>(inputs, result) } + +[] +let ``Map deserialization should reject duplicate keys``() = + let options = new JsonSerializerOptions() + options.AllowDuplicateProperties <- false + + Assert.Throws(fun () -> JsonSerializer.Deserialize>("""{ "1": "a", "1": "b" }""", options) |> ignore) |> ignore + JsonSerializer.Deserialize>("""{ "1": "a", "1": "b" }""") |> ignore // No throw + + Assert.Throws(fun () -> JsonSerializer.Deserialize>("""{ "1": "a", "1": "b" }""", options) |> ignore) |> ignore + JsonSerializer.Deserialize>("""{ "1": "a", "1": "b" }""") |> ignore // No throw diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs index 07c40a9f15e2f3..df27fc881b9229 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs @@ -977,5 +977,19 @@ internal sealed class IgnoreWhenReadingWritingPerson internal partial class SerializeIgnoreReadingWritingJsonSerializerContext : JsonSerializerContext { } + + [Fact] + public static void SupportsDisallowDuplicateProperty() + { + JsonTypeInfo typeInfo = + ContextWithAllowDuplicateProperties.Default.GetTypeInfo(typeof(Dictionary)); + Assert.Throws(() => JsonSerializer.Deserialize("""{"a":1,"a":2}""", typeInfo)); + } + + [JsonSourceGenerationOptions(AllowDuplicateProperties = false)] + [JsonSerializable(typeof(Dictionary))] + internal partial class ContextWithAllowDuplicateProperties : JsonSerializerContext + { + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGenerationOptionsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGenerationOptionsTests.cs index 7693bcaf9884bd..17f5e125a84c1b 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGenerationOptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGenerationOptionsTests.cs @@ -85,6 +85,7 @@ public static void ContextWithAllOptionsSet_GeneratesExpectedOptions() WriteIndented = true, IndentCharacter = '\t', IndentSize = 1, + AllowDuplicateProperties = false, TypeInfoResolver = ContextWithAllOptionsSet.Default, }; @@ -118,7 +119,8 @@ public static void ContextWithAllOptionsSet_GeneratesExpectedOptions() UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, WriteIndented = true, IndentCharacter = '\t', - IndentSize = 1)] + IndentSize = 1, + AllowDuplicateProperties = false)] [JsonSerializable(typeof(PersonStruct))] public partial class ContextWithAllOptionsSet : JsonSerializerContext { } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/CollectionTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/CollectionTests.cs index d54eebb4125752..6e6068a058f845 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/CollectionTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/CollectionTests.cs @@ -121,6 +121,7 @@ public async Task DeserializeAsyncEnumerable() [JsonSerializable(typeof(ClassWithDictionaryAndProperty_DictionaryLast))] [JsonSerializable(typeof(SimpleClassWithDictionaries))] [JsonSerializable(typeof(DictionaryThatOnlyImplementsIDictionaryOfStringTValue))] + [JsonSerializable(typeof(DictionaryThatOnlyImplementsIDictionaryOfStringTValue))] [JsonSerializable(typeof(DictionaryThatOnlyImplementsIDictionaryOfStringTValue))] [JsonSerializable(typeof(DictionaryThatOnlyImplementsIDictionaryOfStringPoco))] [JsonSerializable(typeof(DictionaryThatHasIncompatibleEnumerator))] @@ -464,6 +465,14 @@ public async Task DeserializeAsyncEnumerable() [JsonSerializable(typeof(ReadOnlyMemoryOfTClass))] [JsonSerializable(typeof(MemoryOfTClass))] [JsonSerializable(typeof(ReadOnlyMemoryOfTClass))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(IDictionary))] + [JsonSerializable(typeof(IReadOnlyDictionary))] + [JsonSerializable(typeof(ImmutableDictionary))] + [JsonSerializable(typeof(IImmutableDictionary))] + [JsonSerializable(typeof(ImmutableSortedDictionary))] + [JsonSerializable(typeof(ConcurrentDictionary))] + [JsonSerializable(typeof(GenericIDictionaryWrapper))] internal sealed partial class CollectionTestsContext_Metadata : JsonSerializerContext { } @@ -548,6 +557,7 @@ public CollectionTests_Default() [JsonSerializable(typeof(ClassWithDictionaryAndProperty_DictionaryLast))] [JsonSerializable(typeof(SimpleClassWithDictionaries))] [JsonSerializable(typeof(DictionaryThatOnlyImplementsIDictionaryOfStringTValue))] + [JsonSerializable(typeof(DictionaryThatOnlyImplementsIDictionaryOfStringTValue))] [JsonSerializable(typeof(DictionaryThatOnlyImplementsIDictionaryOfStringTValue))] [JsonSerializable(typeof(DictionaryThatOnlyImplementsIDictionaryOfStringPoco))] [JsonSerializable(typeof(DictionaryThatHasIncompatibleEnumerator))] @@ -876,6 +886,14 @@ public CollectionTests_Default() [JsonSerializable(typeof(ReadOnlyMemoryOfTClass))] [JsonSerializable(typeof(MemoryOfTClass))] [JsonSerializable(typeof(ReadOnlyMemoryOfTClass))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(IDictionary))] + [JsonSerializable(typeof(IReadOnlyDictionary))] + [JsonSerializable(typeof(ImmutableDictionary))] + [JsonSerializable(typeof(IImmutableDictionary))] + [JsonSerializable(typeof(ImmutableSortedDictionary))] + [JsonSerializable(typeof(ConcurrentDictionary))] + [JsonSerializable(typeof(GenericIDictionaryWrapper))] internal sealed partial class CollectionTestsContext_Default : JsonSerializerContext { } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs index 06ff3b6e39bb1c..12a65d54bf8bb7 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs @@ -155,6 +155,9 @@ protected ConstructorTests_Metadata(JsonSerializerWrapper stringWrapper) [JsonSerializable(typeof(ClassWithIgnoredPropertyDefaultParam))] [JsonSerializable(typeof(ClassWithCustomConverterOnCtorParameter))] [JsonSerializable(typeof(ClassWithConflictingCaseInsensitiveProperties))] + [JsonSerializable(typeof(Class_ExtraProperty_ExtData))] + [JsonSerializable(typeof(Class_ExtraProperty_JsonElementDictionaryExtData))] + [JsonSerializable(typeof(Class_ManyParameters_ExtraProperty_ExtData))] internal sealed partial class ConstructorTestsContext_Metadata : JsonSerializerContext { } @@ -305,6 +308,9 @@ public ConstructorTests_Default(JsonSerializerWrapper jsonSerializer) : base(jso [JsonSerializable(typeof(ClassWithIgnoredPropertyDefaultParam))] [JsonSerializable(typeof(ClassWithCustomConverterOnCtorParameter))] [JsonSerializable(typeof(ClassWithConflictingCaseInsensitiveProperties))] + [JsonSerializable(typeof(Class_ExtraProperty_ExtData))] + [JsonSerializable(typeof(Class_ExtraProperty_JsonElementDictionaryExtData))] + [JsonSerializable(typeof(Class_ManyParameters_ExtraProperty_ExtData))] internal sealed partial class ConstructorTestsContext_Default : JsonSerializerContext { } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets index 455a99709805da..5a26cadb6f66e5 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets @@ -39,6 +39,7 @@ + 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 8be93677470ec5..9b2680a3804270 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 @@ -15,6 +15,7 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Threading; +using TestData = System.Text.Json.Serialization.Tests.TestData; namespace System.Text.Json.Tests { @@ -22,6 +23,8 @@ public static class JsonDocumentTests { private static readonly byte[] Utf8Bom = { 0xEF, 0xBB, 0xBF }; + private static readonly JsonDocumentOptions s_noDuplicateParamsOptions = new() { AllowDuplicateProperties = false }; + private static readonly Dictionary s_expectedConcat = new Dictionary(); @@ -596,31 +599,31 @@ private static void DepthFirstAppend(StringBuilder buf, JsonElement element) case JsonValueKind.True: case JsonValueKind.String: case JsonValueKind.Number: + { + buf.Append(element.ToString()); + buf.Append(", "); + break; + } + case JsonValueKind.Object: + { + foreach (JsonProperty prop in element.EnumerateObject()) { - buf.Append(element.ToString()); + buf.Append(prop.Name); buf.Append(", "); - break; + DepthFirstAppend(buf, prop.Value); } - case JsonValueKind.Object: - { - foreach (JsonProperty prop in element.EnumerateObject()) - { - buf.Append(prop.Name); - buf.Append(", "); - DepthFirstAppend(buf, prop.Value); - } - break; - } + break; + } case JsonValueKind.Array: + { + foreach (JsonElement child in element.EnumerateArray()) { - foreach (JsonElement child in element.EnumerateArray()) - { - DepthFirstAppend(buf, child); - } - - break; + DepthFirstAppend(buf, child); } + + break; + } } } @@ -3482,7 +3485,7 @@ void CheckPropertyCountAndArrayLengthAgainstEnumerateMethods(JsonElement elem) CheckPropertyCountAndArrayLengthAgainstEnumerateMethods(prop.Value); } } - else if (elem.ValueKind == JsonValueKind.Array) + else if (elem.ValueKind == JsonValueKind.Array) { Assert.Equal(elem.EnumerateArray().Count(), elem.GetArrayLength()); foreach (JsonElement item in elem.EnumerateArray()) @@ -3834,6 +3837,165 @@ public static void DeserializeNullAsNullLiteral() Assert.NotNull(jsonDocument); Assert.Equal(JsonValueKind.Null, jsonDocument.RootElement.ValueKind); } + + [Fact] + public static void JsonMarshal_GetRawUtf8Value_DisposedDocument_ThrowsObjectDisposedException() + { + JsonDocument jDoc = JsonDocument.Parse("{}"); + JsonElement element = jDoc.RootElement; + jDoc.Dispose(); + + Assert.Throws(() => JsonMarshal.GetRawUtf8Value(element)); + } + + [Theory] + [MemberData(nameof(TestData.DuplicatePropertyJsonPayloads), MemberType = typeof(TestData))] + public static void ParseJsonDocumentWithDuplicateProperties(string jsonPayload, bool isValidJson = false) + { + AssertDuplicateProperty(jsonPayload, isValidJson); + } + + [Theory] + [MemberData(nameof(TestData.DuplicatePropertyJsonPayloads), MemberType = typeof(TestData))] + public static void ParseJsonDocumentArrayWithDuplicateProperties(string jsonPayload, bool isValidJson = false) + { + jsonPayload = $"[{jsonPayload}]"; + AssertDuplicateProperty(jsonPayload, isValidJson); + } + + [Theory] + [MemberData(nameof(TestData.DuplicatePropertyJsonPayloads), MemberType = typeof(TestData))] + public static void ParseJsonDocumentDeeplyNestedWithDuplicateProperties(string jsonPayload, bool isValidJson = false) + { + jsonPayload = $$"""{"p0":{"p1":{"p2":{"p3":{"p4":{"p5":{"p6":{"p7":{"p8":{"p9":{{jsonPayload}}} } } } } } } } } }"""; + AssertDuplicateProperty(jsonPayload, isValidJson); + } + + [Theory] + [MemberData(nameof(TestData.DuplicatePropertyJsonPayloads), MemberType = typeof(TestData))] + public static void ParseJsonDocumentClassWithDuplicateProperties(string jsonPayload, bool isValidJson = false) + { + jsonPayload = $$"""{"Object":{{jsonPayload}}}"""; + AssertDuplicateProperty(jsonPayload, isValidJson); + } + + [Theory] + [InlineData(3, 0)] + [InlineData(3, 2)] + [InlineData(100, 0)] + [InlineData(100, 99)] + public static void ParseJsonDocumentObjectWithDuplicateProperties(int count, int insertPosition) + { + string json = CreatePayload(count, insertPosition, "p1"); + AssertDuplicateProperty(json, isValidJson: false); + } + + [Theory] + [InlineData(3, 0)] + [InlineData(3, 2)] + [InlineData(100, 0)] + [InlineData(100, 99)] + public static void ParseJsonDocumentEscapeWithDuplicateProperties(int count, int insertPosition) + { + string json = CreatePayload(count, insertPosition, """p\u0031"""); + AssertDuplicateProperty(json, isValidJson: false); + } + + [Theory] + [InlineData(3, 0)] + [InlineData(3, 2)] + [InlineData(100, 0)] + [InlineData(100, 99)] + public static void ParseJsonDocumentObjectWithNoDuplicateProperties(int count, int insertPosition) + { + string json = CreatePayload(count, insertPosition, "notduplicate"); + AssertDuplicateProperty(json, isValidJson: true); + } + + [Theory] + [InlineData(3, 0)] + [InlineData(3, 2)] + [InlineData(100, 0)] + [InlineData(100, 99)] + public static void ParseJsonDocumentEscapeWithNoDuplicateProperties(int count, int insertPosition) + { + string json = CreatePayload(count, insertPosition, """notduplicate\u0030\u0030"""); + AssertDuplicateProperty(json, isValidJson: true); + } + + private static string CreatePayload(int count, int insertPosition, string insertProperty) + { + StringBuilder builder = new(); + builder.Append("{"); + for (int i = 0; i < count; i++) + { + if (i > 0) + builder.AppendLine(","); + + if (i == insertPosition) + builder.Append($""" "{insertProperty}":1"""); + else + builder.Append($""" "p{i}":1"""); + } + builder.Append("}"); + return builder.ToString(); + } + + private static void AssertDuplicateProperty(string jsonPayload, bool isValidJson) + { + if (isValidJson) + { + using (JsonDocument.Parse(jsonPayload, s_noDuplicateParamsOptions)) { } // Assert no throw + } + else + { + Assert.Throws(() => JsonDocument.Parse(jsonPayload, s_noDuplicateParamsOptions)); + } + + using (JsonDocument.Parse(jsonPayload)) { } // Assert no throw + } + + [Fact] + public static void ParseJsonDuplicatePropertiesErrorMessage() + { + string json = """{"a":1,"a":1}"""; + AssertExtensions.ThrowsContains( + () => JsonDocument.Parse(json, s_noDuplicateParamsOptions), + "a"); + } + + [Fact] + public static void ParseJsonDuplicatePropertiesErrorMessageLong() + { + string json = """{"12345678901234567":1,"12345678901234567":1}"""; + AssertExtensions.ThrowsContains( + () => JsonDocument.Parse(json, s_noDuplicateParamsOptions), + "123456789012345..."); + + json = """{"123456789012345":1,"123456789012345":1}"""; + AssertExtensions.ThrowsContains( + () => JsonDocument.Parse(json, s_noDuplicateParamsOptions), + "123456789012345"); + + json = """{"1234567890123456":1,"1234567890123456":1}"""; + AssertExtensions.ThrowsContains( + () => JsonDocument.Parse(json, s_noDuplicateParamsOptions), + "123456789012345..."); + } + + [Fact] + public static void ParseJsonDuplicatePropertiesErrorMessageEscaped() + { + string json = """{"0":1,"\u0030":1}"""; + AssertExtensions.ThrowsContains( + () => JsonDocument.Parse(json, s_noDuplicateParamsOptions), + "'\u0030'"); + + json = """{"\u0030":1,"0":1}"""; + AssertExtensions.ThrowsContains( + () => JsonDocument.Parse(json, s_noDuplicateParamsOptions), + "'0'"); + } } public class ThrowOnReadStream : MemoryStream diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs index ac8258de22d71e..6325889252f655 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs @@ -1631,5 +1631,92 @@ class ClassWithObjectExtensionData [JsonExtensionData] public JsonObject ExtensionData { get; set; } } + + [Theory] + [MemberData(nameof(TestData.DuplicatePropertyJsonPayloads), MemberType = typeof(TestData))] + public static void JsonObject_DuplicatePropertyThrows(string jsonPayload, bool isValidJson = false) + { + AssertDuplicatePropertyThrows(jsonPayload, isValidJson); + AssertDuplicatePropertyThrows(jsonPayload, isValidJson); + } + + [Theory] + [MemberData(nameof(TestData.DuplicatePropertyJsonPayloads), MemberType = typeof(TestData))] + public static void JsonObject_DuplicatePropertyThrows_NestedInArray(string jsonPayload, bool isValidJson = false) + { + jsonPayload = $"[{jsonPayload}]"; + AssertDuplicatePropertyThrows(jsonPayload, isValidJson); + AssertDuplicatePropertyThrows(jsonPayload, isValidJson); + } + + [Theory] + [MemberData(nameof(TestData.DuplicatePropertyJsonPayloads), MemberType = typeof(TestData))] + public static void JsonObject_DuplicatePropertyThrows_NestedDeeply(string jsonPayload, bool isValidJson = false) + { + jsonPayload = $$"""{"p0":{"p1":{"p2":{"p3":{"p4":{"p5":{"p6":{"p7":{"p8":{"p9":{{jsonPayload}}} } } } } } } } } }"""; + AssertDuplicatePropertyThrows(jsonPayload, isValidJson); + AssertDuplicatePropertyThrows(jsonPayload, isValidJson); + } + + private static void AssertDuplicatePropertyThrows(string jsonPayload, bool isValidJson) + where T : JsonNode + { + if (isValidJson) + { + T node = JsonSerializer.Deserialize(jsonPayload, JsonTestSerializerOptions.DisallowDuplicateProperties); + JsonNode.DeepEquals(node, node); // Assert no throw + + node = JsonSerializer.Deserialize(jsonPayload); + JsonNode.DeepEquals(node, node); // Assert no throw + } + else + { + AssertExtensions.ThrowsContains( + () => JsonSerializer.Deserialize(jsonPayload, JsonTestSerializerOptions.DisallowDuplicateProperties), + "An item with the same key has already been added."); + + // Default options don't throw on deserialize but will throw when accessed + T node = JsonSerializer.Deserialize(jsonPayload); + AssertExtensions.ThrowsContains( + () => JsonNode.DeepEquals(node, node), + "An item with the same key has already been added."); + } + } + + [Fact] + public static void JsonObject_DuplicatePropertyCaseInsensitiveThrows() + { + JsonSerializerOptions options = JsonTestSerializerOptions.DisallowDuplicatePropertiesIgnoringCase; + + string jsonPayload = """{"a":1,"A":2}"""; + + _ = JsonSerializer.Deserialize(jsonPayload); // Assert no throw + AssertExtensions.ThrowsContains( + () => JsonSerializer.Deserialize(jsonPayload, options), + "An item with the same key has already been added."); + + _ = JsonSerializer.Deserialize(jsonPayload); // Assert no throw + AssertExtensions.ThrowsContains( + () => JsonSerializer.Deserialize(jsonPayload, options), + "An item with the same key has already been added."); + } + + [Fact] + public static void JsonObject_NestedDuplicatePropertyCaseInsensitiveThrows() + { + JsonSerializerOptions options = JsonTestSerializerOptions.DisallowDuplicatePropertiesIgnoringCase; + + string jsonPayload = """[{"a":1,"A":2}]"""; + + _ = JsonSerializer.Deserialize(jsonPayload); // Assert no throw + AssertExtensions.ThrowsContains( + () => JsonSerializer.Deserialize(jsonPayload, options), + "An item with the same key has already been added."); + + _ = JsonSerializer.Deserialize(jsonPayload); // Assert no throw + AssertExtensions.ThrowsContains( + () => JsonSerializer.Deserialize(jsonPayload, options), + "An item with the same key has already been added."); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs index d283789a6b322d..fb9079c916672f 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs @@ -383,6 +383,7 @@ public static void JsonSerializerOptions_EqualityComparer_ChangingAnySettingShou yield return (GetProp(nameof(JsonSerializerOptions.IndentSize)), 1); yield return (GetProp(nameof(JsonSerializerOptions.ReferenceHandler)), ReferenceHandler.Preserve); yield return (GetProp(nameof(JsonSerializerOptions.TypeInfoResolver)), new DefaultJsonTypeInfoResolver()); + yield return (GetProp(nameof(JsonSerializerOptions.AllowDuplicateProperties)), false /* true is default */); static PropertyInfo GetProp(string name) { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonDocumentTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonDocumentTests.cs index fc736eee2b9980..4a06803f06e100 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonDocumentTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonDocumentTests.cs @@ -69,5 +69,77 @@ public void ReadJsonDocumentFromStream(int defaultBufferSize) stream = new MemoryStream(data); Assert.Throws(() => JsonSerializer.DeserializeAsync(stream, new JsonSerializerOptions { DefaultBufferSize = defaultBufferSize }).Result); } + + [Theory] + [MemberData(nameof(TestData.DuplicatePropertyJsonPayloads), MemberType = typeof(TestData))] + public void DserializeJsonDocumentWithDuplicateProperties(string jsonPayload, bool isValidJson = false) + { + AssertDuplicateProperty(jsonPayload, isValidJson); + } + + [Theory] + [MemberData(nameof(TestData.DuplicatePropertyJsonPayloads), MemberType = typeof(TestData))] + public void DserializeJsonDocumentArrayWithDuplicateProperties(string jsonPayload, bool isValidJson = false) + { + jsonPayload = $"[{jsonPayload}]"; + AssertDuplicateProperty(jsonPayload, isValidJson); + } + + [Theory] + [MemberData(nameof(TestData.DuplicatePropertyJsonPayloads), MemberType = typeof(TestData))] + public static void DserializeJsonDocumentDeeplyNestedWithDuplicateProperties(string jsonPayload, bool isValidJson = false) + { + jsonPayload = $$"""{"p0":{"p1":{"p2":{"p3":{"p4":{"p5":{"p6":{"p7":{"p8":{"p9":{{jsonPayload}}} } } } } } } } } }"""; + AssertDuplicateProperty(jsonPayload, isValidJson); + } + + [Theory] + [MemberData(nameof(TestData.DuplicatePropertyJsonPayloads), MemberType = typeof(TestData))] + public void DserializeJsonDocumentClassWithDuplicateProperties(string jsonPayload, bool isValidJson = false) + { + jsonPayload = $$"""{"Object":{{jsonPayload}}}"""; + AssertDuplicateProperty(jsonPayload, isValidJson); + } + + private static void AssertDuplicateProperty(string jsonPayload, bool isValidJson) + { + if (isValidJson) + { + _ = JsonSerializer.Deserialize(jsonPayload, JsonTestSerializerOptions.DisallowDuplicateProperties); // Assert no throw + } + else + { + Exception ex = Assert.Throws(() => JsonSerializer.Deserialize(jsonPayload, JsonTestSerializerOptions.DisallowDuplicateProperties)); + Assert.Contains("Duplicate", ex.Message); + } + + _ = JsonSerializer.Deserialize(jsonPayload); // Assert no throw + } + + [Fact] + public void DserializeJsonDocument_CaseInsensitiveWithDuplicatePropertiesNoThrow() + { + string jsonPayload = """{"a": 1, "A": 2}"""; + + JsonSerializerOptions options = JsonTestSerializerOptions.DisallowDuplicatePropertiesIgnoringCase; + JsonDocument doc = JsonSerializer.Deserialize(jsonPayload, options); + + // JsonDocument is always case-sensitive + Assert.Equal(1, doc.RootElement.GetProperty("a").GetInt32()); + Assert.Equal(2, doc.RootElement.GetProperty("A").GetInt32()); + } + + [Fact] + public void DserializeJsonDocumentWrapper_CaseInsensitiveWithDuplicatePropertiesNoThrow() + { + string jsonPayload = """{"document": {"a": 1, "A": 2}}"""; + + JsonSerializerOptions options = JsonTestSerializerOptions.DisallowDuplicatePropertiesIgnoringCase; + JsonDocumentClass obj = JsonSerializer.Deserialize(jsonPayload, options); + + // JsonDocument is always case-sensitive + Assert.Equal(1, obj.Document.RootElement.GetProperty("a").GetInt32()); + Assert.Equal(2, obj.Document.RootElement.GetProperty("A").GetInt32()); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonElementTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonElementTests.cs index c0bb9039964b4a..4b84b44d274cd9 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonElementTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonElementTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -292,5 +293,77 @@ private static JsonDocument CreateDeepJsonDocument(int depth) JsonDocumentOptions options = new JsonDocumentOptions { MaxDepth = depth }; return JsonDocument.Parse(bufferWriter.WrittenSpan.ToArray(), options); } + + [Theory] + [MemberData(nameof(TestData.DuplicatePropertyJsonPayloads), MemberType = typeof(TestData))] + public void DserializeJsonElementWithDuplicateProperties(string jsonPayload, bool isValidJson = false) + { + AssertDuplicateProperty(jsonPayload, isValidJson); + } + + [Theory] + [MemberData(nameof(TestData.DuplicatePropertyJsonPayloads), MemberType = typeof(TestData))] + public void DserializeJsonElementArrayWithDuplicateProperties(string jsonPayload, bool isValidJson = false) + { + jsonPayload = $"[{jsonPayload}]"; + AssertDuplicateProperty(jsonPayload, isValidJson); + } + + [Theory] + [MemberData(nameof(TestData.DuplicatePropertyJsonPayloads), MemberType = typeof(TestData))] + public static void DserializeJsonElementDeeplyNestedWithDuplicateProperties(string jsonPayload, bool isValidJson = false) + { + jsonPayload = $$"""{"p0":{"p1":{"p2":{"p3":{"p4":{"p5":{"p6":{"p7":{"p8":{"p9":{{jsonPayload}}} } } } } } } } } }"""; + AssertDuplicateProperty(jsonPayload, isValidJson); + } + + [Theory] + [MemberData(nameof(TestData.DuplicatePropertyJsonPayloads), MemberType = typeof(TestData))] + public void DserializeJsonElementClassWithDuplicateProperties(string jsonPayload, bool isValidJson = false) + { + jsonPayload = $$"""{"Object":{{jsonPayload}}}"""; + AssertDuplicateProperty(jsonPayload, isValidJson); + } + + private static void AssertDuplicateProperty(string jsonPayload, bool isValidJson) + { + if (isValidJson) + { + _ = JsonSerializer.Deserialize(jsonPayload, JsonTestSerializerOptions.DisallowDuplicateProperties); // Assert no throw + } + else + { + Exception ex = Assert.Throws(() => JsonSerializer.Deserialize(jsonPayload, JsonTestSerializerOptions.DisallowDuplicateProperties)); + Assert.Contains("Duplicate", ex.Message); + } + + _ = JsonSerializer.Deserialize(jsonPayload); // Assert no throw + } + + [Fact] + public void DserializeJsonElement_CaseInsensitiveWithDuplicatePropertiesNoThrow() + { + string jsonPayload = """{"a": 1, "A": 2}"""; + + JsonSerializerOptions options = JsonTestSerializerOptions.DisallowDuplicatePropertiesIgnoringCase; + JsonElement obj = JsonSerializer.Deserialize(jsonPayload, options); + + // JsonElement is always case-sensitive + Assert.Equal(1, obj.GetProperty("a").GetInt32()); + Assert.Equal(2, obj.GetProperty("A").GetInt32()); + } + + [Fact] + public void DserializeJsonElementWrapper_CaseInsensitiveWithDuplicatePropertiesNoThrow() + { + string jsonPayload = """{"object": {"a": 1, "A": 2}}"""; + + JsonSerializerOptions options = JsonTestSerializerOptions.DisallowDuplicatePropertiesIgnoringCase; + JsonElementClass obj = JsonSerializer.Deserialize(jsonPayload, options); + + // JsonElement is always case-sensitive + Assert.Equal(1, obj.Object.GetProperty("a").GetInt32()); + Assert.Equal(2, obj.Object.GetProperty("A").GetInt32()); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Object.ReadTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Object.ReadTests.cs index 5acd48fa4e6e47..2846dc7a5653f3 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Object.ReadTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Object.ReadTests.cs @@ -672,5 +672,42 @@ public static void ReadObjectWithNumberHandling(JsonUnknownTypeHandling unknownT object result = JsonSerializer.Deserialize(@"{ ""key"" : ""42"" }", options); Assert.IsAssignableFrom(expectedType, result); } + + [Theory] + [InlineData("""{ "MyInt32" : 42, "MyInt32" : 42 }""")] + [InlineData("""{ "MyInt32Array" : null, "MyInt32Array" : null }""")] + public static void ReadSimpleObjectWithDuplicateProperties(string payload) + { + JsonSerializerOptions options = JsonTestSerializerOptions.DisallowDuplicateProperties; + + Exception ex = Assert.Throws(() => JsonSerializer.Deserialize(payload, options)); + Assert.Contains("Duplicate", ex.Message); + + _ = JsonSerializer.Deserialize(payload); // Assert no throw + } + + [Theory] + [InlineData("""{ "MyData" : null, "MyData" : null }""")] + [InlineData("""{ "MyData" : {}, "MyData" : {} }""")] + public static void ReadNestedObjectWithDuplicateProperties(string payload) + { + JsonSerializerOptions options = JsonTestSerializerOptions.DisallowDuplicateProperties; + + Exception ex = Assert.Throws(() => JsonSerializer.Deserialize(payload, options)); + Assert.Contains("Duplicate", ex.Message); + + _ = JsonSerializer.Deserialize(payload); // Assert no throw + } + + [Theory] + [InlineData("""{ "MyInt32" : 42, "myInt32" : 42 }""")] + [InlineData("""{ "MyInt32Array" : null, "myInt32Array" : null }""")] + public static void ReadSimpleObjectWithDuplicatePropertiesCaseInsensitive(string payload) + { + var options = JsonTestSerializerOptions.DisallowDuplicatePropertiesIgnoringCase; + + Exception ex = Assert.Throws(() => JsonSerializer.Deserialize(payload, options)); + Assert.Contains("Duplicate", ex.Message); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs index aaa53e4d7d89c1..840a5e1079e50f 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs @@ -68,6 +68,7 @@ public static void SetOptionsFail() Assert.False(options.WriteIndented); Assert.False(options.RespectNullableAnnotations); Assert.False(options.RespectRequiredConstructorParameters); + Assert.True(options.AllowDuplicateProperties); TestIListNonThrowingOperationsWhenImmutable(options.Converters, tc); TestIListNonThrowingOperationsWhenImmutable(options.TypeInfoResolverChain, options.TypeInfoResolver); @@ -89,6 +90,7 @@ public static void SetOptionsFail() Assert.Throws(() => options.TypeInfoResolver = options.TypeInfoResolver); Assert.Throws(() => options.RespectNullableAnnotations = options.RespectNullableAnnotations); Assert.Throws(() => options.RespectRequiredConstructorParameters = options.RespectRequiredConstructorParameters); + Assert.Throws(() => options.AllowDuplicateProperties = options.AllowDuplicateProperties); TestIListThrowingOperationsWhenImmutable(options.Converters, tc); TestIListThrowingOperationsWhenImmutable(options.TypeInfoResolverChain, options.TypeInfoResolver); @@ -1889,5 +1891,18 @@ public static IEnumerable GetTypeInfo_ResultsAreGeneric_Values() static object[] WrapArgs(T value, string json) => new object[] { value, json }; } + + [Fact] + public static void AllowDuplicateProperties_RespectsSetting() + { + var options = new JsonSerializerOptions { AllowDuplicateProperties = false }; + string json = "{\"a\":1,\"a\":2}"; + Assert.Throws(() => JsonSerializer.Deserialize>(json, options)); + + options = new JsonSerializerOptions { AllowDuplicateProperties = true }; + var result = JsonSerializer.Deserialize>(json, options); + Assert.Single(result); + Assert.Equal(2, result["a"]); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index 201b569a9eecb0..c2017a91ef1316 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -70,6 +70,7 @@ +