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 bc75757b165af8..2b18b602cae192 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -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)] @@ -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 utf8Key); + } [System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple=false)] public sealed partial class JsonExtensionDataAttribute : System.Text.Json.Serialization.JsonAttribute { 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 d0d1e3ba9841c5..982fae73ebd7fd 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -123,6 +123,8 @@ 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/Serialization/Converters/Collection/JsonDictionaryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs index 6a50c841387186..fe8b98734a7a73 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.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.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization.Metadata; @@ -16,6 +17,12 @@ internal abstract class JsonDictionaryConverter : 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; + } } /// @@ -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 keyConverter, ref Utf8JsonReader reader, scoped ref ReadStack state, JsonSerializerOptions options) + { + if (options.DictionaryKeyFilter is JsonDictionaryKeyFilter keyFilter) + { + ReadOnlySpan span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; + if (reader.ValueIsEscaped) + { + span = JsonReaderHelper.GetUnescapedSpan(span); + } + if (keyFilter.IgnoreKey(span)) + { + return default; + } + } + + TKey key; + 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 keyConverter, ref Utf8JsonReader reader, scoped ref ReadStack state, JsonSerializerOptions options) { TKey key; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonDictionaryKeyFilter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonDictionaryKeyFilter.cs new file mode 100644 index 00000000000000..7a177057e94879 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonDictionaryKeyFilter.cs @@ -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; + +/// +/// Determines what JSON keys should be ignored on dictionary deserialization. +/// +public abstract class JsonDictionaryKeyFilter +{ + /// + /// Initializes a new instance of . + /// + protected JsonDictionaryKeyFilter() { } + + /// + /// Returns the key filter that ignores any metadata keys starting with $, such as `$schema`. + /// + public static JsonDictionaryKeyFilter IgnoreMetadataNames { get; } = new JsonIgnoreMetadataNamesDictionaryKeyFilter(); + + /// + /// When overridden in a derived class, ignore keys according to filter. + /// + /// The UTF8 string with key name to filter. + /// true to ignore that key. + public abstract bool IgnoreKey(ReadOnlySpan utf8Key); +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIgnoreMetadataNamesDictionaryKeyFilter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIgnoreMetadataNamesDictionaryKeyFilter.cs new file mode 100644 index 00000000000000..fadc4c91787e66 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIgnoreMetadataNamesDictionaryKeyFilter.cs @@ -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; + +/// +/// Determines dictionary key filter to ignore metadata keys starting with $, such as `$schema`. +/// +internal sealed class JsonIgnoreMetadataNamesDictionaryKeyFilter : JsonDictionaryKeyFilter +{ + + /// + /// Ignores any metadata keys starting with $, such as `$schema`. + /// + public override bool IgnoreKey(ReadOnlySpan utf8Key) => utf8Key.StartsWith("$"u8); +} 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 9380884e9f7935..7f0708be36dcc2 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 @@ -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; _dictionaryKeyPolicy = options._dictionaryKeyPolicy; _jsonPropertyNamingPolicy = options._jsonPropertyNamingPolicy; _readCommentHandling = options._readCommentHandling; @@ -296,6 +298,28 @@ public JavaScriptEncoder? Encoder } } + /// + /// Specifies the filter used to ignore keys + /// when deserializing. + /// + /// + /// This property can be set to + /// to ignore any keys starting with $, such as `$schema`. + /// It is used when deserializing. + /// + public JsonDictionaryKeyFilter? DictionaryKeyFilter + { + get + { + return _dictionaryKeyFilter; + } + set + { + VerifyMutable(); + _dictionaryKeyFilter = value; + } + } + /// /// Specifies the policy used to convert a key's name to another format, such as camel-casing. /// diff --git a/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.KeyFilter.cs b/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.KeyFilter.cs new file mode 100644 index 00000000000000..b559940dd99474 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.KeyFilter.cs @@ -0,0 +1,276 @@ +// 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.Threading.Tasks; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public abstract partial class CollectionTests + { + [Fact] + public async Task DictionaryKeyFilter_MetadataNamesDeserialize() + { + var options = new JsonSerializerOptions + { + DictionaryKeyFilter = JsonDictionaryKeyFilter.IgnoreMetadataNames // Ignore metadata keys starting with $, such as `$schema`. + }; + + const string JsonString = @"[{""$metadata"":-1,""Key1"":1,""Key2"":2},{""Key1"":3,""Key2"":4,""$metadata"":5}]"; + + // Without filter, deserialize all keys. + Dictionary[] obj = await Serializer.DeserializeWrapper[]>(JsonString); + + Assert.Equal(2, obj.Length); + + Assert.Equal(3, obj[0].Count); + Assert.Equal(-1, obj[0]["$metadata"]); + Assert.Equal(1, obj[0]["Key1"]); + Assert.Equal(2, obj[0]["Key2"]); + + Assert.Equal(3, obj[1].Count); + Assert.Equal(3, obj[1]["Key1"]); + Assert.Equal(4, obj[1]["Key2"]); + Assert.Equal(5, obj[1]["$metadata"]); + + // With key filter, ignore metadata keys. + obj = await Serializer.DeserializeWrapper[]>(JsonString, options); + + Assert.Equal(2, obj.Length); + + Assert.Equal(2, obj[0].Count); + Assert.Equal(1, obj[0]["Key1"]); + Assert.Equal(2, obj[0]["Key2"]); + + Assert.Equal(2, obj[1].Count); + Assert.Equal(3, obj[1]["Key1"]); + Assert.Equal(4, obj[1]["Key2"]); + } + + [Fact] + public async Task DictionaryKeyFilter_IgnoreOnSerialize() + { + var options = new JsonSerializerOptions() + { + DictionaryKeyFilter = JsonDictionaryKeyFilter.IgnoreMetadataNames // Ignore metadata keys starting with $, such as `$schema`. + }; + + Dictionary[] obj = new Dictionary[] + { + new Dictionary() { { "$metadata", -1 }, { "Key1", 1 }, { "Key2", 2 } }, + new Dictionary() { { "Key1", 3 }, { "Key2", 4 }, { "$metadata", 5 } }, + }; + + const string Json = @"[{""$metadata"":1,""Key1"":1,""Key2"":2},{""Key1"":3,""Key2"":4,""$metadata"":5}]"; + + // Without key filter, serialize keys as they are. + string json = await Serializer.SerializeWrapper(obj); + Assert.Equal(Json, json); + + // Ensure we ignore key filter and serialize keys as they are. + json = await Serializer.SerializeWrapper(obj, options); + Assert.Equal(Json, json); + } + + [Fact] + public async Task DictionaryKeyFilter_IgnoreForExtensionData() + { + var options = new JsonSerializerOptions + { + DictionaryKeyFilter = JsonDictionaryKeyFilter.IgnoreMetadataNames // Ignore metadata keys starting with $, such as `$schema`. + }; + + const string JsonString = @"{""$metadata"":-1,""Key1"":1, ""Key2"":2,""$metadata3"":3,""$metadataUndefined"":," + + @"""$metadataObject"":{},""$metadataArray"":[,{},[],""$metadataString"",4,true,false,null]," + + @"""$metadataString"":""$metadataString"",""$metadataTrue"":true,""$metadataFalse"":false,""$metadataNull"":null}"; + + // Ensure we ignore dictionary key filter for extension data and deserialize all keys. + ClassWithExtensionData myClass = await Serializer.DeserializeWrapper(JsonString, options); + Assert.Equal(-1, myClass.ExtensionData["$metadata"].GetInt32()); + Assert.Equal(1, myClass.ExtensionData["Key1"].GetInt32()); + Assert.Equal(2, myClass.ExtensionData["Key2"].GetInt32()); + Assert.Equal(3, myClass.ExtensionData["$metadata3"].GetInt32()); + Assert.Equal(JsonValueKind.Undefined, myClass.ExtensionData["$metadataUndefined"].ValueKind); + Assert.Equal(JsonValueKind.Object, myClass.ExtensionData["$metadataObject"].ValueKind); + Assert.Equal(JsonValueKind.Array, myClass.ExtensionData["$metadataArray"].ValueKind); + Assert.Equal(8, myClass.ExtensionData["$metadataArray"].GetArrayLength()); + Assert.Equal(JsonValueKind.String, myClass.ExtensionData["$metadataString"].ValueKind); + Assert.Equal("$metadataString", myClass.ExtensionData["$metadataString"].GetString()); + Assert.Equal(JsonValueKind.Number, myClass.ExtensionData["$metadataNumber"].ValueKind); + Assert.Equal(4, myClass.ExtensionData["$metadataString"].GetInt32()); + Assert.Equal(JsonValueKind.True, myClass.ExtensionData["$metadataTrue"].ValueKind); + Assert.Equal(JsonValueKind.False, myClass.ExtensionData["$metadataFalse"].ValueKind); + Assert.Equal(JsonValueKind.Null, myClass.ExtensionData["$metadataNull"].ValueKind); + } + + //[Fact] + //public async Task DictionaryKeyFilter_MetadataNames_Any_Values() + //{ + // var options = new JsonSerializerOptions + // { + // DictionaryKeyFilter = JsonDictionaryKeyFilter.IgnoreMetadataNames // Ignore metadata keys starting with $, such as `$schema`. + // }; + + // const string JsonString = @"{""$metadata"":-1,""Key1"":1, ""Key2"":2,""$metadata3"":3," + + // @"""$metadataArray"":[{},[]],""$metadataFalse"":false,""$metadataNull"":null," + + // @"""$metadataObject"":{},""$metadataString"":""$metadataString""," + + // @"""$metadataTrue"":true,""$metadataUndefined"":}"; + + // Assert.ThrowsAsync(() => Serializer.SerializeWrapper(dict, options)); + + // // Without key filter, deserialize throw exception. + // Dictionary[] obj = await Serializer.DeserializeWrapper[]>(JsonString); + + // // With key filter, ignore metadata keys. + // obj = await Serializer.DeserializeWrapper[]>(JsonString, options); + + // Assert.Equal(2, obj.Length); + + // Assert.Equal(2, obj[0].Count); + // Assert.Equal(1, obj[0]["Key1"]); + // Assert.Equal(2, obj[0]["Key2"]); + + // Assert.Equal(2, obj[1].Count); + // Assert.Equal(3, obj[1]["Key1"]); + // Assert.Equal(4, obj[1]["Key2"]); + //} + + // [Fact] + // public async Task CustomNameDeserialize() + // { + // var options = new JsonSerializerOptions + // { + // DictionaryKeyPolicy = new UppercaseNamingPolicy() // e.g. myint -> MYINT. + // }; + + + // // Without key policy, deserialize keys as they are. + // Dictionary obj = await Serializer.DeserializeWrapper>(@"{""myint"":1}"); + // Assert.Equal(1, obj["myint"]); + + // // Ensure we ignore key policy and deserialize keys as they are. + // obj = await Serializer.DeserializeWrapper>(@"{""myint"":1}", options); + // Assert.Equal(1, obj["myint"]); + // } + + // [Fact] + //#if BUILDING_SOURCE_GENERATOR_TESTS + // [ActiveIssue("Need extension data support.")] + //#endif + // public async Task DeserializationWithJsonExtensionDataAttribute_IgoneDictionaryKeyPolicy() + // { + // var expectedJson = @"{""KeyInt"":1000,""KeyString"":""text"",""KeyBool"":true,""KeyObject"":{},""KeyList"":[],""KeyDictionary"":{}}"; + // var obj = new ClassWithExtensionDataProperty(); + // obj.Data = new Dictionary() + // { + // { "KeyInt", 1000 }, + // { "KeyString", "text" }, + // { "KeyBool", true }, + // { "KeyObject", new object() }, + // { "KeyList", new List() }, + // { "KeyDictionary", new Dictionary() } + // }; + // string json = await Serializer.SerializeWrapper(obj, new JsonSerializerOptions() + // { + // DictionaryKeyPolicy = JsonNamingPolicy.CamelCase + // }); + // Assert.Equal(expectedJson, json); + // } + + // private class ClassWithExtensionDataProperty + // { + // [JsonExtensionData] + // public Dictionary Data { get; set; } + // } + + // [Fact] + // public async Task CamelCaseSerialize_ForTypedDictionary_ApplyDictionaryKeyPolicy() + // { + // const string JsonCamel = @"{""keyDict"":{""Name"":""text"",""Number"":1000,""isValid"":true,""Values"":[1,2,3]}}"; + // var obj = new Dictionary() + // { + // { "KeyDict", CreateCustomObject() } + // }; + // var json = await Serializer.SerializeWrapper(obj, new JsonSerializerOptions() + // { + // DictionaryKeyPolicy = JsonNamingPolicy.CamelCase + // }); + + // Assert.Equal(JsonCamel, json); + // } + + // public class CustomClass + // { + // public string Name { get; set; } + // public int Number { get; set; } + // public bool isValid { get; set; } + // public List Values { get; set; } + // } + + // private static CustomClass CreateCustomObject() + // { + // return new CustomClass { Name = "text", Number = 1000, isValid = true, Values = new List() { 1, 2, 3 } }; + // } + + // [Fact] + // public async Task CamelCaseSerialize_ForNestedTypedDictionary_ApplyDictionaryKeyPolicy() + // { + // const string JsonCamel = @"{""keyDict"":{""nestedKeyDict"":{""Name"":""text"",""Number"":1000,""isValid"":true,""Values"":[1,2,3]}}}"; + // var options = new JsonSerializerOptions + // { + // DictionaryKeyPolicy = JsonNamingPolicy.CamelCase + // }; + // var obj = new Dictionary>(){ + // { "KeyDict", new Dictionary() + // {{ "NestedKeyDict", CreateCustomObject() }} + // }}; + // var json = await Serializer.SerializeWrapper(obj, new JsonSerializerOptions() + // { + // DictionaryKeyPolicy = JsonNamingPolicy.CamelCase + // }); + + // Assert.Equal(JsonCamel, json); + // } + + // public class TestClassWithDictionary + // { + // public Dictionary Data { get; set; } + // } + + // [Fact] + // public async Task CamelCaseSerialize_ForClassWithDictionaryProperty_ApplyDictionaryKeyPolicy() + // { + // const string JsonCamel = @"{""Data"":{""keyObj"":{""Name"":""text"",""Number"":1000,""isValid"":true,""Values"":[1,2,3]}}}"; + // var obj = new TestClassWithDictionary(); + // obj.Data = new Dictionary { + // {"KeyObj", CreateCustomObject() } + // }; + // var json = await Serializer.SerializeWrapper(obj, new JsonSerializerOptions() + // { + // DictionaryKeyPolicy = JsonNamingPolicy.CamelCase + // }); + // Assert.Equal(JsonCamel, json); + // } + + // [Fact] + // public async Task CamelCaseSerialize_ForKeyValuePairWithDictionaryValue_ApplyDictionaryKeyPolicy() + // { + // const string JsonCamel = @"{""Key"":""KeyPair"",""Value"":{""keyDict"":{""Name"":""text"",""Number"":1000,""isValid"":true,""Values"":[1,2,3]}}}"; + // var options = new JsonSerializerOptions + // { + // DictionaryKeyPolicy = JsonNamingPolicy.CamelCase + // }; + // var obj = new KeyValuePair> + // ("KeyPair", new Dictionary { + // {"KeyDict", CreateCustomObject() } + // }); + // var json = await Serializer.SerializeWrapper(obj, new JsonSerializerOptions() + // { + // DictionaryKeyPolicy = JsonNamingPolicy.CamelCase + // }); + + // Assert.Equal(JsonCamel, json); + // } + } +} diff --git a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.cs index dfe51ae6439134..cb581c1e01d5ea 100644 --- a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.cs +++ b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.cs @@ -1989,6 +1989,24 @@ public class Poco public int Id { get; set; } } + public class IgnoreNamesWithAtCharDictionaryKeyFilter : JsonDictionaryKeyFilter + { + public override bool IgnoreKey(ReadOnlySpan utf8Key) + => utf8Key.IndexOf("@"u8) < 0; + } + + public class IgnoreAllNamesDictionaryKeyFilter : JsonDictionaryKeyFilter + { + public override bool IgnoreKey(ReadOnlySpan utf8Key) + => true; + } + + public class AcceptAllNamesDictionaryKeyFilter : JsonDictionaryKeyFilter + { + public override bool IgnoreKey(ReadOnlySpan utf8Key) + => false; + } + public class UppercaseNamingPolicy : JsonNamingPolicy { public override string ConvertName(string name) 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 979defdc010ac5..5d054e9466ca86 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 @@ -357,6 +357,7 @@ public static void JsonSerializerOptions_EqualityComparer_ChangingAnySettingShou yield return (GetProp(nameof(JsonSerializerOptions.AllowTrailingCommas)), true); yield return (GetProp(nameof(JsonSerializerOptions.DefaultBufferSize)), 42); yield return (GetProp(nameof(JsonSerializerOptions.Encoder)), JavaScriptEncoder.UnsafeRelaxedJsonEscaping); + yield return (GetProp(nameof(JsonSerializerOptions.DictionaryKeyFilter)), JsonDictionaryKeyFilter.IgnoreMetadataNames); yield return (GetProp(nameof(JsonSerializerOptions.DictionaryKeyPolicy)), JsonNamingPolicy.CamelCase); yield return (GetProp(nameof(JsonSerializerOptions.IgnoreNullValues)), true); yield return (GetProp(nameof(JsonSerializerOptions.DefaultIgnoreCondition)), JsonIgnoreCondition.WhenWritingDefault); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/DictionaryKeyFilterUnitTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/DictionaryKeyFilterUnitTests.cs new file mode 100644 index 00000000000000..2697f005fad809 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/DictionaryKeyFilterUnitTests.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static class DictionaryKeyFilterUnitTests + { + [Fact] + public static void IgnoreMetadataNamesTest() + { + Assert.True(IgnoreKey("$key")); + Assert.True(IgnoreKey("$ key")); + Assert.True(IgnoreKey("$_key")); + Assert.True(IgnoreKey("$\0key")); + Assert.True(IgnoreKey("$ ")); + Assert.True(IgnoreKey("$_")); + Assert.True(IgnoreKey("$\0")); + Assert.True(IgnoreKey("$")); + + Assert.False(IgnoreKey("key")); + Assert.False(IgnoreKey("key$")); + Assert.False(IgnoreKey(" $key")); + Assert.False(IgnoreKey("_$key")); + Assert.False(IgnoreKey("\0$key")); + Assert.False(IgnoreKey(" $")); + Assert.False(IgnoreKey("_$")); + Assert.False(IgnoreKey("\0$")); + Assert.False(IgnoreKey("")); + + static bool IgnoreKey(string name) + => JsonDictionaryKeyFilter.IgnoreMetadataNames.IgnoreKey(Encoding.UTF8.GetBytes(name)); + } + } +} 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 575cd0eaf028ae..205515347f9fa7 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 @@ -57,6 +57,7 @@ public static void SetOptionsFail() // Verify defaults and ensure getters do not throw. Assert.False(options.AllowTrailingCommas); Assert.Equal(16 * 1024, options.DefaultBufferSize); + Assert.Null(options.DictionaryKeyFilter); Assert.Null(options.DictionaryKeyPolicy); Assert.Null(options.Encoder); Assert.False(options.IgnoreNullValues); @@ -74,6 +75,7 @@ public static void SetOptionsFail() // Setters should always throw; we don't check to see if the value is the same or not. Assert.Throws(() => options.AllowTrailingCommas = options.AllowTrailingCommas); Assert.Throws(() => options.DefaultBufferSize = options.DefaultBufferSize); + Assert.Throws(() => options.DictionaryKeyFilter = options.DictionaryKeyFilter); Assert.Throws(() => options.DictionaryKeyPolicy = options.DictionaryKeyPolicy); Assert.Throws(() => options.Encoder = JavaScriptEncoder.Default); Assert.Throws(() => options.IgnoreNullValues = options.IgnoreNullValues); @@ -1226,6 +1228,10 @@ and not nameof(JsonSerializerOptions.IsReadOnly)) // Property is not structural { options.Encoder = JavaScriptEncoder.Default; } + else if (propertyType == typeof(JsonDictionaryKeyFilter)) + { + options.DictionaryKeyFilter = JsonDictionaryKeyFilter.IgnoreMetadataNames; + } else if (propertyType == typeof(JsonNamingPolicy)) { options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; 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 676a0bd87c0107..3d6ececd5ec636 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 @@ -33,6 +33,7 @@ + @@ -164,6 +165,7 @@ +