-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support filtering property names on dictionary deserialization #87868
Changes from all commits
ea6d4c7
14c1e4d
91e66c9
f8da4b9
cd441ee
4bd8bec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.Buffers; | ||
using System.Diagnostics; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Text.Json.Serialization.Metadata; | ||
|
@@ -16,6 +17,12 @@ internal abstract class JsonDictionaryConverter<TDictionary> : JsonResumableConv | |
private protected sealed override ConverterStrategy GetDefaultConverterStrategy() => ConverterStrategy.Dictionary; | ||
|
||
protected internal abstract bool OnWriteResume(Utf8JsonWriter writer, TDictionary dictionary, JsonSerializerOptions options, ref WriteStack state); | ||
|
||
internal override void ConfigureJsonTypeInfo(JsonTypeInfo jsonTypeInfo, JsonSerializerOptions options) | ||
{ | ||
base.ConfigureJsonTypeInfo(jsonTypeInfo, options); | ||
//jsonTypeInfo.Options.DictionaryKeyFilter = options.DictionaryKeyFilter; | ||
} | ||
} | ||
|
||
/// <summary> | ||
|
@@ -120,13 +127,17 @@ internal sealed override bool OnTryRead( | |
Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); | ||
|
||
state.Current.JsonPropertyInfo = keyTypeInfo.PropertyInfoForTypeInfo; | ||
TKey key = ReadDictionaryKey(_keyConverter, ref reader, ref state, options); | ||
TKey? key = TryReadDictionaryKey(_keyConverter, ref reader, ref state, options); | ||
|
||
// Read the value and add. | ||
// Read the value | ||
reader.ReadWithVerify(); | ||
state.Current.JsonPropertyInfo = elementTypeInfo.PropertyInfoForTypeInfo; | ||
TValue? element = _valueConverter.Read(ref reader, ElementType, options); | ||
Add(key, element!, options, ref state); | ||
if (key is not null) | ||
{ | ||
// Get the value from the converter and add it. | ||
state.Current.JsonPropertyInfo = elementTypeInfo.PropertyInfoForTypeInfo; | ||
TValue? element = _valueConverter.Read(ref reader, ElementType, options); | ||
Add(key, element!, options, ref state); | ||
} | ||
} | ||
} | ||
else | ||
|
@@ -145,14 +156,17 @@ internal sealed override bool OnTryRead( | |
// Read method would have thrown if otherwise. | ||
Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); | ||
state.Current.JsonPropertyInfo = keyTypeInfo.PropertyInfoForTypeInfo; | ||
TKey key = ReadDictionaryKey(_keyConverter, ref reader, ref state, options); | ||
TKey? key = TryReadDictionaryKey(_keyConverter, ref reader, ref state, options); | ||
|
||
// Read the value | ||
reader.ReadWithVerify(); | ||
|
||
// Get the value from the converter and add it. | ||
state.Current.JsonPropertyInfo = elementTypeInfo.PropertyInfoForTypeInfo; | ||
_valueConverter.TryRead(ref reader, ElementType, options, ref state, out TValue? element, out _); | ||
Add(key, element!, options, ref state); | ||
if (key is not null) | ||
{ | ||
// Get the value from the converter and add it. | ||
state.Current.JsonPropertyInfo = elementTypeInfo.PropertyInfoForTypeInfo; | ||
_valueConverter.TryRead(ref reader, ElementType, options, ref state, out TValue? element, out _); | ||
Add(key, element!, options, ref state); | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -306,6 +320,38 @@ internal sealed override bool OnTryRead( | |
value = (TDictionary)state.Current.ReturnValue!; | ||
return true; | ||
|
||
static TKey? TryReadDictionaryKey(JsonConverter<TKey> keyConverter, ref Utf8JsonReader reader, scoped ref ReadStack state, JsonSerializerOptions options) | ||
{ | ||
if (options.DictionaryKeyFilter is JsonDictionaryKeyFilter keyFilter) | ||
{ | ||
ReadOnlySpan<byte> span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; | ||
if (reader.ValueIsEscaped) | ||
{ | ||
span = JsonReaderHelper.GetUnescapedSpan(span); | ||
} | ||
if (keyFilter.IgnoreKey(span)) | ||
{ | ||
return default; | ||
} | ||
} | ||
|
||
TKey key; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is repeating the logic of the existing |
||
string unescapedPropertyNameAsString = reader.GetString()!; | ||
state.Current.JsonPropertyNameAsString = unescapedPropertyNameAsString; // Copy key name for JSON Path support in case of error. | ||
|
||
// Special case string to avoid calling GetString twice and save one allocation. | ||
if (keyConverter.IsInternalConverter && keyConverter.Type == typeof(string)) | ||
{ | ||
key = (TKey)(object)unescapedPropertyNameAsString; | ||
} | ||
else | ||
{ | ||
key = keyConverter.ReadAsPropertyNameCore(ref reader, keyConverter.Type, options); | ||
} | ||
|
||
return key; | ||
} | ||
|
||
static TKey ReadDictionaryKey(JsonConverter<TKey> keyConverter, ref Utf8JsonReader reader, scoped ref ReadStack state, JsonSerializerOptions options) | ||
{ | ||
TKey key; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// 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.Serialization; | ||
|
||
/// <summary> | ||
/// Determines what JSON keys should be ignored on dictionary deserialization. | ||
/// </summary> | ||
public abstract class JsonDictionaryKeyFilter | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of <see cref="JsonDictionaryKeyFilter"/>. | ||
/// </summary> | ||
protected JsonDictionaryKeyFilter() { } | ||
|
||
/// <summary> | ||
/// Returns the key filter that ignores any metadata keys starting with $, such as `$schema`. | ||
/// </summary> | ||
public static JsonDictionaryKeyFilter IgnoreMetadataNames { get; } = new JsonIgnoreMetadataNamesDictionaryKeyFilter(); | ||
|
||
/// <summary> | ||
/// When overridden in a derived class, ignore keys according to filter. | ||
/// </summary> | ||
/// <param name="utf8Key">The UTF8 string with key name to filter.</param> | ||
/// <returns>true to ignore that key.</returns> | ||
public abstract bool IgnoreKey(ReadOnlySpan<byte> utf8Key); | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,16 @@ | ||||||||||
// 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.Serialization; | ||||||||||
|
||||||||||
/// <summary> | ||||||||||
/// Determines dictionary key filter to ignore metadata keys starting with $, such as `$schema`. | ||||||||||
/// </summary> | ||||||||||
internal sealed class JsonIgnoreMetadataNamesDictionaryKeyFilter : JsonDictionaryKeyFilter | ||||||||||
{ | ||||||||||
|
||||||||||
/// <summary> | ||||||||||
/// Ignores any metadata keys starting with $, such as `$schema`. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than ignoring every key starting with Lines 19 to 22 in 5ca6826
|
||||||||||
/// </summary> | ||||||||||
public override bool IgnoreKey(ReadOnlySpan<byte> utf8Key) => utf8Key.StartsWith("$"u8); | ||||||||||
} |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -54,6 +54,7 @@ public static JsonSerializerOptions Default | |||
|
||||
// For any new option added, adding it to the options copied in the copy constructor below must be considered. | ||||
private IJsonTypeInfoResolver? _typeInfoResolver; | ||||
private JsonDictionaryKeyFilter? _dictionaryKeyFilter; | ||||
private JsonNamingPolicy? _dictionaryKeyPolicy; | ||||
private JsonNamingPolicy? _jsonPropertyNamingPolicy; | ||||
private JsonCommentHandling _readCommentHandling; | ||||
|
@@ -105,6 +106,7 @@ public JsonSerializerOptions(JsonSerializerOptions options) | |||
// 2. _typeInfoResolverChain can be created lazily as it relies on | ||||
// _typeInfoResolver as its source of truth. | ||||
|
||||
_dictionaryKeyFilter = options._dictionaryKeyFilter; | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should also update the EqualityComparer implementation in Line 480 in 5ca6826
|
||||
_dictionaryKeyPolicy = options._dictionaryKeyPolicy; | ||||
_jsonPropertyNamingPolicy = options._jsonPropertyNamingPolicy; | ||||
_readCommentHandling = options._readCommentHandling; | ||||
|
@@ -296,6 +298,28 @@ public JavaScriptEncoder? Encoder | |||
} | ||||
} | ||||
|
||||
/// <summary> | ||||
/// Specifies the filter used to ignore <see cref="System.Collections.IDictionary"/> keys | ||||
/// when deserializing. | ||||
/// </summary> | ||||
/// <remarks> | ||||
/// This property can be set to <see cref="JsonDictionaryKeyFilter.IgnoreMetadataNames"/> | ||||
/// to ignore any keys starting with $, such as `$schema`. | ||||
/// It is used when deserializing. | ||||
/// </remarks> | ||||
public JsonDictionaryKeyFilter? DictionaryKeyFilter | ||||
{ | ||||
get | ||||
{ | ||||
return _dictionaryKeyFilter; | ||||
} | ||||
set | ||||
{ | ||||
VerifyMutable(); | ||||
_dictionaryKeyFilter = value; | ||||
} | ||||
} | ||||
|
||||
/// <summary> | ||||
/// Specifies the policy used to convert a <see cref="System.Collections.IDictionary"/> key's name to another format, such as camel-casing. | ||||
/// </summary> | ||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like this isn't doing anything?