Skip to content
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

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { }
public static System.Text.Json.JsonSerializerOptions Default { [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications."), System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] get { throw null; } }
public int DefaultBufferSize { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonIgnoreCondition DefaultIgnoreCondition { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonDictionaryKeyFilter? DictionaryKeyFilter { get { throw null; } set { } }
public System.Text.Json.JsonNamingPolicy? DictionaryKeyPolicy { get { throw null; } set { } }
public System.Text.Encodings.Web.JavaScriptEncoder? Encoder { get { throw null; } set { } }
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
Expand Down Expand Up @@ -927,6 +928,11 @@ public JsonDerivedTypeAttribute(System.Type derivedType, string typeDiscriminato
public System.Type DerivedType { get { throw null; } }
public object? TypeDiscriminator { get { throw null; } }
}
public abstract class JsonDictionaryKeyFilter
{
public static System.Text.Json.Serialization.JsonDictionaryKeyFilter IgnoreMetadataNames { get { throw null; } }
public abstract bool IgnoreKey(System.ReadOnlySpan<byte> utf8Key);
}
[System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple=false)]
public sealed partial class JsonExtensionDataAttribute : System.Text.Json.Serialization.JsonAttribute
{
Expand Down
2 changes: 2 additions & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="System\Text\Json\Serialization\IJsonOnDeserializing.cs" />
<Compile Include="System\Text\Json\Serialization\IJsonOnSerialized.cs" />
<Compile Include="System\Text\Json\Serialization\IJsonOnSerializing.cs" />
<Compile Include="System\Text\Json\Serialization\JsonDictionaryKeyFilter.cs" />
<Compile Include="System\Text\Json\Serialization\JsonIgnoreMetadataNamesDictionaryKeyFilter.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.Document.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.Element.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.Node.cs" />
Expand Down
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;
Expand All @@ -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)
Copy link
Member

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?

{
base.ConfigureJsonTypeInfo(jsonTypeInfo, options);
//jsonTypeInfo.Options.DictionaryKeyFilter = options.DictionaryKeyFilter;
}
}

/// <summary>
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}
}
}
Expand Down Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is repeating the logic of the existing ReadDictionaryKey method. It seems like you could simply extract the DictionaryKeyFilter logic into a standalone helper?

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;
Expand Down
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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than ignoring every key starting with $, I would recommend only checking for metadata property names that have built-in support in JSON:

private static readonly byte[] s_idPropertyName = "$id"u8.ToArray();
private static readonly byte[] s_refPropertyName = "$ref"u8.ToArray();
private static readonly byte[] s_typePropertyName = "$type"u8.ToArray();
private static readonly byte[] s_valuesPropertyName = "$values"u8.ToArray();

/// </summary>
public override bool IgnoreKey(ReadOnlySpan<byte> utf8Key) => utf8Key.StartsWith("$"u8);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should also update the EqualityComparer implementation in JsonSerializerOptions.Caching to account for the new property:

private sealed class EqualityComparer : IEqualityComparer<JsonSerializerOptions>

_dictionaryKeyPolicy = options._dictionaryKeyPolicy;
_jsonPropertyNamingPolicy = options._jsonPropertyNamingPolicy;
_readCommentHandling = options._readCommentHandling;
Expand Down Expand Up @@ -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>
Expand Down
Loading