Skip to content

Commit de610d0

Browse files
Option to disallow duplicate JSON properties (#115856)
1 parent 3aa0c97 commit de610d0

File tree

64 files changed

+1705
-134
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+1705
-134
lines changed

src/libraries/System.Text.Json/Common/JsonHelpers.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ public static bool TryAdd<TKey, TValue>(this Dictionary<TKey, TValue> dictionary
2626
return false;
2727
}
2828

29+
/// <summary>
30+
/// netstandard/netfx polyfill for IDictionary.TryAdd
31+
/// </summary>
32+
public static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue value) where TKey : notnull
33+
{
34+
if (!dictionary.ContainsKey(key))
35+
{
36+
dictionary[key] = value;
37+
return true;
38+
}
39+
40+
return false;
41+
}
42+
2943
/// <summary>
3044
/// netstandard/netfx polyfill for Queue.TryDequeue
3145
/// </summary>

src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,5 +170,10 @@ public JsonSourceGenerationOptionsAttribute(JsonSerializerDefaults defaults)
170170
/// Specifies the default value of <see cref="JsonSerializerOptions.NewLine"/> when set.
171171
/// </summary>
172172
public string? NewLine { get; set; }
173+
174+
/// <summary>
175+
/// Specifies the default value of <see cref="JsonSerializerOptions.AllowDuplicateProperties"/> when set.
176+
/// </summary>
177+
public bool AllowDuplicateProperties { get; set; }
173178
}
174179
}

src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,9 @@ private static void GetLogicForDefaultSerializerOptionsInit(SourceGenerationOpti
11881188
writer.WriteLine('{');
11891189
writer.Indentation++;
11901190

1191+
if (optionsSpec.AllowDuplicateProperties is bool allowDuplicateProperties)
1192+
writer.WriteLine($"AllowDuplicateProperties = {FormatBoolLiteral(allowDuplicateProperties)},");
1193+
11911194
if (optionsSpec.AllowOutOfOrderMetadataProperties is bool allowOutOfOrderMetadataProperties)
11921195
writer.WriteLine($"AllowOutOfOrderMetadataProperties = {FormatBoolLiteral(allowOutOfOrderMetadataProperties)},");
11931196

src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN
287287
bool? writeIndented = null;
288288
char? indentCharacter = null;
289289
int? indentSize = null;
290+
bool? allowDuplicateProperties = null;
290291

291292
if (attributeData.ConstructorArguments.Length > 0)
292293
{
@@ -412,6 +413,10 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN
412413
generationMode = (JsonSourceGenerationMode)namedArg.Value.Value!;
413414
break;
414415

416+
case nameof(JsonSourceGenerationOptionsAttribute.AllowDuplicateProperties):
417+
allowDuplicateProperties = (bool)namedArg.Value.Value!;
418+
break;
419+
415420
default:
416421
throw new InvalidOperationException();
417422
}
@@ -446,6 +451,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN
446451
WriteIndented = writeIndented,
447452
IndentCharacter = indentCharacter,
448453
IndentSize = indentSize,
454+
AllowDuplicateProperties = allowDuplicateProperties,
449455
};
450456
}
451457

src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ public sealed record SourceGenerationOptionsSpec
6666

6767
public required int? IndentSize { get; init; }
6868

69+
public required bool? AllowDuplicateProperties { get; init; }
70+
6971
public JsonKnownNamingPolicy? GetEffectivePropertyNamingPolicy()
7072
=> PropertyNamingPolicy ?? (Defaults is JsonSerializerDefaults.Web ? JsonKnownNamingPolicy.CamelCase : null);
7173
}

src/libraries/System.Text.Json/ref/System.Text.Json.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public void WriteTo(System.Text.Json.Utf8JsonWriter writer) { }
3838
public partial struct JsonDocumentOptions
3939
{
4040
private int _dummyPrimitive;
41+
public bool AllowDuplicateProperties { get { throw null; } set { } }
4142
public bool AllowTrailingCommas { readonly get { throw null; } set { } }
4243
public System.Text.Json.JsonCommentHandling CommentHandling { readonly get { throw null; } set { } }
4344
public int MaxDepth { readonly get { throw null; } set { } }
@@ -393,6 +394,7 @@ public sealed partial class JsonSerializerOptions
393394
public JsonSerializerOptions() { }
394395
public JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults defaults) { }
395396
public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { }
397+
public bool AllowDuplicateProperties { get { throw null; } set { } }
396398
public bool AllowOutOfOrderMetadataProperties { get { throw null; } set { } }
397399
public bool AllowTrailingCommas { get { throw null; } set { } }
398400
public System.Collections.Generic.IList<System.Text.Json.Serialization.JsonConverter> Converters { get { throw null; } }
@@ -1139,6 +1141,7 @@ public sealed partial class JsonSourceGenerationOptionsAttribute : System.Text.J
11391141
{
11401142
public JsonSourceGenerationOptionsAttribute() { }
11411143
public JsonSourceGenerationOptionsAttribute(System.Text.Json.JsonSerializerDefaults defaults) { }
1144+
public bool AllowDuplicateProperties { get { throw null; } set { } }
11421145
public bool AllowOutOfOrderMetadataProperties { get { throw null; } set { } }
11431146
public bool AllowTrailingCommas { get { throw null; } set { } }
11441147
public System.Type[]? Converters { get { throw null; } set { } }

src/libraries/System.Text.Json/src/Resources/Strings.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,4 +827,13 @@
827827
<data name="CannotMixEncodings" xml:space="preserve">
828828
<value>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}'.</value>
829829
</data>
830+
<data name="DuplicatePropertiesNotAllowed_JsonPropertyInfo" xml:space="preserve">
831+
<value>Duplicate property '{0}' encountered during deserialization of type '{1}'.</value>
832+
</data>
833+
<data name="DuplicatePropertiesNotAllowed_NameSpan" xml:space="preserve">
834+
<value>Duplicate property '{0}' encountered during deserialization.</value>
835+
</data>
836+
<data name="DuplicatePropertiesNotAllowed" xml:space="preserve">
837+
<value>Duplicate properties not allowed during deserialization.</value>
838+
</data>
830839
</root>

src/libraries/System.Text.Json/src/System.Text.Json.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
5151
<Compile Include="System\Runtime\InteropServices\JsonMarshal.cs" />
5252
<Compile Include="System\Text\Json\AppContextSwitchHelper.cs" />
5353
<Compile Include="System\Text\Json\BitStack.cs" />
54+
<Compile Include="System\Text\Json\Document\JsonDocument.PropertyNameSet.cs" />
5455
<Compile Include="System\Text\Json\Document\JsonDocument.cs" />
5556
<Compile Include="System\Text\Json\Document\JsonDocument.DbRow.cs" />
5657
<Compile Include="System\Text\Json\Document\JsonDocument.MetadataDb.cs" />
@@ -342,6 +343,8 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
342343
<Compile Include="System\ReflectionExtensions.cs" />
343344
<Compile Include="$(CommonPath)System\Obsoletions.cs" Link="Common\System\Obsoletions.cs" />
344345
<Compile Include="System\ThrowHelper.cs" />
346+
347+
<Compile Include="$(CoreLibSharedDir)System\Marvin.cs" Link="Common\System\Marvin.cs" />
345348
</ItemGroup>
346349

347350
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
@@ -418,6 +421,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
418421
<Reference Include="System.Runtime.InteropServices" />
419422
<Reference Include="System.Runtime.Intrinsics" />
420423
<Reference Include="System.Runtime.Loader" />
424+
<Reference Include="System.Security.Cryptography" />
421425
<Reference Include="System.Text.Encoding.Extensions" />
422426
<Reference Include="System.Text.Encodings.Web" />
423427
<Reference Include="System.Text.RegularExpressions" />

src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public sealed partial class JsonDocument
4646
/// </exception>
4747
public static JsonDocument Parse(ReadOnlyMemory<byte> utf8Json, JsonDocumentOptions options = default)
4848
{
49-
return Parse(utf8Json, options.GetReaderOptions());
49+
return Parse(utf8Json, options.GetReaderOptions(), allowDuplicateProperties: options.AllowDuplicateProperties);
5050
}
5151

5252
/// <summary>
@@ -80,7 +80,7 @@ public static JsonDocument Parse(ReadOnlySequence<byte> utf8Json, JsonDocumentOp
8080

8181
if (utf8Json.IsSingleSegment)
8282
{
83-
return Parse(utf8Json.First, readerOptions);
83+
return Parse(utf8Json.First, readerOptions, allowDuplicateProperties: options.AllowDuplicateProperties);
8484
}
8585

8686
int length = checked((int)utf8Json.Length);
@@ -89,7 +89,11 @@ public static JsonDocument Parse(ReadOnlySequence<byte> utf8Json, JsonDocumentOp
8989
try
9090
{
9191
utf8Json.CopyTo(utf8Bytes.AsSpan());
92-
return Parse(utf8Bytes.AsMemory(0, length), readerOptions, utf8Bytes);
92+
return Parse(
93+
utf8Bytes.AsMemory(0, length),
94+
readerOptions,
95+
utf8Bytes,
96+
allowDuplicateProperties: options.AllowDuplicateProperties);
9397
}
9498
catch
9599
{
@@ -123,7 +127,11 @@ public static JsonDocument Parse(Stream utf8Json, JsonDocumentOptions options =
123127
Debug.Assert(drained.Array != null);
124128
try
125129
{
126-
return Parse(drained.AsMemory(), options.GetReaderOptions(), drained.Array);
130+
return Parse(
131+
drained.AsMemory(),
132+
options.GetReaderOptions(),
133+
drained.Array,
134+
allowDuplicateProperties: options.AllowDuplicateProperties);
127135
}
128136
catch
129137
{
@@ -140,7 +148,8 @@ internal static JsonDocument ParseRented(PooledByteBufferWriter utf8Json, JsonDo
140148
utf8Json.WrittenMemory,
141149
options.GetReaderOptions(),
142150
extraRentedArrayPoolBytes: null,
143-
extraPooledByteBufferWriter: utf8Json);
151+
extraPooledByteBufferWriter: utf8Json,
152+
allowDuplicateProperties: options.AllowDuplicateProperties);
144153
}
145154

146155
internal static JsonDocument ParseValue(Stream utf8Json, JsonDocumentOptions options)
@@ -157,15 +166,21 @@ internal static JsonDocument ParseValue(Stream utf8Json, JsonDocumentOptions opt
157166
drained.AsSpan().Clear();
158167
ArrayPool<byte>.Shared.Return(drained.Array);
159168

160-
return ParseUnrented(owned.AsMemory(), options.GetReaderOptions());
169+
return ParseUnrented(
170+
owned.AsMemory(),
171+
options.GetReaderOptions(),
172+
allowDuplicateProperties: options.AllowDuplicateProperties);
161173
}
162174

163175
internal static JsonDocument ParseValue(ReadOnlySpan<byte> utf8Json, JsonDocumentOptions options)
164176
{
165177
byte[] owned = new byte[utf8Json.Length];
166178
utf8Json.CopyTo(owned);
167179

168-
return ParseUnrented(owned.AsMemory(), options.GetReaderOptions());
180+
return ParseUnrented(
181+
owned.AsMemory(),
182+
options.GetReaderOptions(),
183+
allowDuplicateProperties: options.AllowDuplicateProperties);
169184
}
170185

171186
internal static JsonDocument ParseValue(string json, JsonDocumentOptions options)
@@ -209,7 +224,11 @@ private static async Task<JsonDocument> ParseAsyncCore(
209224
Debug.Assert(drained.Array != null);
210225
try
211226
{
212-
return Parse(drained.AsMemory(), options.GetReaderOptions(), drained.Array);
227+
return Parse(
228+
drained.AsMemory(),
229+
options.GetReaderOptions(),
230+
drained.Array,
231+
allowDuplicateProperties: options.AllowDuplicateProperties);
213232
}
214233
catch
215234
{
@@ -235,7 +254,10 @@ internal static async Task<JsonDocument> ParseAsyncCoreUnrented(
235254
drained.AsSpan().Clear();
236255
ArrayPool<byte>.Shared.Return(drained.Array);
237256

238-
return ParseUnrented(owned.AsMemory(), options.GetReaderOptions());
257+
return ParseUnrented(
258+
owned.AsMemory(),
259+
options.GetReaderOptions(),
260+
allowDuplicateProperties: options.AllowDuplicateProperties);
239261
}
240262

241263
/// <summary>
@@ -271,7 +293,8 @@ public static JsonDocument Parse([StringSyntax(StringSyntaxAttribute.Json)] Read
271293
return Parse(
272294
utf8Bytes.AsMemory(0, actualByteCount),
273295
options.GetReaderOptions(),
274-
utf8Bytes);
296+
utf8Bytes,
297+
allowDuplicateProperties: options.AllowDuplicateProperties);
275298
}
276299
catch
277300
{
@@ -304,7 +327,10 @@ internal static JsonDocument ParseValue(ReadOnlyMemory<char> json, JsonDocumentO
304327
ArrayPool<byte>.Shared.Return(utf8Bytes);
305328
}
306329

307-
return ParseUnrented(owned.AsMemory(), options.GetReaderOptions());
330+
return ParseUnrented(
331+
owned.AsMemory(),
332+
options.GetReaderOptions(),
333+
allowDuplicateProperties: options.AllowDuplicateProperties);
308334
}
309335

310336
/// <summary>
@@ -406,9 +432,12 @@ public static bool TryParseValue(ref Utf8JsonReader reader, [NotNullWhen(true)]
406432
/// <exception cref="JsonException">
407433
/// A value could not be read from the reader.
408434
/// </exception>
409-
public static JsonDocument ParseValue(ref Utf8JsonReader reader)
435+
public static JsonDocument ParseValue(ref Utf8JsonReader reader) =>
436+
ParseValue(ref reader, allowDuplicateProperties: true);
437+
438+
internal static JsonDocument ParseValue(ref Utf8JsonReader reader, bool allowDuplicateProperties)
410439
{
411-
bool ret = TryParseValue(ref reader, out JsonDocument? document, shouldThrow: true, useArrayPools: true);
440+
bool ret = TryParseValue(ref reader, out JsonDocument? document, shouldThrow: true, useArrayPools: true, allowDuplicateProperties);
412441

413442
Debug.Assert(ret, "TryParseValue returned false with shouldThrow: true.");
414443
Debug.Assert(document != null, "null document returned with shouldThrow: true.");
@@ -419,7 +448,8 @@ internal static bool TryParseValue(
419448
ref Utf8JsonReader reader,
420449
[NotNullWhen(true)] out JsonDocument? document,
421450
bool shouldThrow,
422-
bool useArrayPools)
451+
bool useArrayPools,
452+
bool allowDuplicateProperties = true)
423453
{
424454
JsonReaderState state = reader.CurrentState;
425455
CheckSupportedOptions(state.Options, nameof(reader));
@@ -629,7 +659,7 @@ internal static bool TryParseValue(
629659
valueSpan.CopyTo(rentedSpan);
630660
}
631661

632-
document = Parse(rented.AsMemory(0, length), state.Options, rented);
662+
document = Parse(rented.AsMemory(0, length), state.Options, rented, allowDuplicateProperties: allowDuplicateProperties);
633663
}
634664
catch
635665
{
@@ -654,7 +684,7 @@ internal static bool TryParseValue(
654684
owned = valueSpan.ToArray();
655685
}
656686

657-
document = ParseUnrented(owned, state.Options, reader.TokenType);
687+
document = ParseUnrented(owned, state.Options, reader.TokenType, allowDuplicateProperties: allowDuplicateProperties);
658688
}
659689

660690
return true;
@@ -688,18 +718,28 @@ private static JsonDocument Parse(
688718
ReadOnlyMemory<byte> utf8Json,
689719
JsonReaderOptions readerOptions,
690720
byte[]? extraRentedArrayPoolBytes = null,
691-
PooledByteBufferWriter? extraPooledByteBufferWriter = null)
721+
PooledByteBufferWriter? extraPooledByteBufferWriter = null,
722+
bool allowDuplicateProperties = true)
692723
{
693724
ReadOnlySpan<byte> utf8JsonSpan = utf8Json.Span;
694725
var database = MetadataDb.CreateRented(utf8Json.Length, convertToAlloc: false);
695726
var stack = new StackRowStack(JsonDocumentOptions.DefaultMaxDepth * StackRow.Size);
727+
JsonDocument document;
696728

697729
try
698730
{
699731
Parse(utf8JsonSpan, readerOptions, ref database, ref stack);
732+
document = new JsonDocument(utf8Json, database, extraRentedArrayPoolBytes, extraPooledByteBufferWriter, isDisposable: true);
733+
734+
if (!allowDuplicateProperties)
735+
{
736+
ValidateNoDuplicateProperties(document);
737+
}
700738
}
701739
catch
702740
{
741+
// The caller returns any resources they rented, so all we need to do is dispose the database.
742+
// Specifically: don't dispose the document as that will result in double return of the rented array.
703743
database.Dispose();
704744
throw;
705745
}
@@ -708,13 +748,14 @@ private static JsonDocument Parse(
708748
stack.Dispose();
709749
}
710750

711-
return new JsonDocument(utf8Json, database, extraRentedArrayPoolBytes, extraPooledByteBufferWriter);
751+
return document;
712752
}
713753

714754
private static JsonDocument ParseUnrented(
715755
ReadOnlyMemory<byte> utf8Json,
716756
JsonReaderOptions readerOptions,
717-
JsonTokenType tokenType = JsonTokenType.None)
757+
JsonTokenType tokenType = JsonTokenType.None,
758+
bool allowDuplicateProperties = true)
718759
{
719760
// These tokens should already have been processed.
720761
Debug.Assert(
@@ -746,7 +787,14 @@ private static JsonDocument ParseUnrented(
746787
}
747788
}
748789

749-
return new JsonDocument(utf8Json, database, isDisposable: false);
790+
JsonDocument document = new JsonDocument(utf8Json, database, isDisposable: false);
791+
792+
if (!allowDuplicateProperties)
793+
{
794+
ValidateNoDuplicateProperties(document);
795+
}
796+
797+
return document;
750798
}
751799

752800
private static ArraySegment<byte> ReadToEnd(Stream stream)

0 commit comments

Comments
 (0)