[release/6.0-rc1] Support special characters in [JsonPropertyName] (#58075)
authorgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Wed, 25 Aug 2021 15:46:30 +0000 (08:46 -0700)
committerGitHub <noreply@github.com>
Wed, 25 Aug 2021 15:46:30 +0000 (08:46 -0700)
* Support special characters in [JsonPropertyName]

* Make gen'd property name readonly; other misc

Co-authored-by: Steve Harter <steveharter@users.noreply.github.com>
src/libraries/System.Text.Json/gen/ContextGenerationSpec.cs
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs
src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs
src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyNameTests.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ExtensionDataTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyNameTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj

index 545d562..13e9c74 100644 (file)
@@ -32,9 +32,11 @@ namespace System.Text.Json.SourceGeneration
         public HashSet<TypeGenerationSpec> TypesWithMetadataGenerated { get; } = new();
 
         /// <summary>
-        /// Cache of runtime property names (statically determined) found accross the object graph of the JsonSerializerContext.
+        /// Cache of runtime property names (statically determined) found across the object graph of the JsonSerializerContext.
+        /// The dictionary Key is the JSON property name, and the Value is the variable name which is the same as the property
+        /// name except for cases where special characters are used with [JsonPropertyName].
         /// </summary>
-        public HashSet<string> RuntimePropertyNames { get; } = new();
+        public Dictionary<string, string> RuntimePropertyNames { get; } = new();
 
         public string ContextTypeRef => ContextType.GetCompilableName();
     }
index 75d8cdd..fafe991 100644 (file)
@@ -9,6 +9,7 @@ using System.Text.Json;
 using System.Text.Json.Reflection;
 using System.Text.Json.Serialization;
 using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
 using Microsoft.CodeAnalysis.Text;
 
 namespace System.Text.Json.SourceGeneration
@@ -803,15 +804,16 @@ private static {JsonParameterInfoValuesTypeRef}[] {typeGenerationSpec.TypeInfoPr
                     TypeGenerationSpec propertyTypeSpec = propertyGenSpec.TypeGenerationSpec;
 
                     string runtimePropName = propertyGenSpec.RuntimePropertyName;
+                    string propVarName = propertyGenSpec.PropertyNameVarName;
 
                     // Add the property names to the context-wide cache; we'll generate the source to initialize them at the end of generation.
-                    _currentContext.RuntimePropertyNames.Add(runtimePropName);
+                    Debug.Assert(!_currentContext.RuntimePropertyNames.TryGetValue(runtimePropName, out string? existingName) || existingName == propVarName);
+                    _currentContext.RuntimePropertyNames.TryAdd(runtimePropName, propVarName);
 
                     Type propertyType = propertyTypeSpec.Type;
-                    string propName = $"{runtimePropName}PropName";
                     string? objectRef = castingRequiredForProps ? $"(({propertyGenSpec.DeclaringTypeRef}){ValueVarName})" : ValueVarName;
                     string propValue = $"{objectRef}.{propertyGenSpec.ClrName}";
-                    string methodArgs = $"{propName}, {propValue}";
+                    string methodArgs = $"{propVarName}, {propValue}";
 
                     string? methodToCall = GetWriterMethod(propertyType);
 
@@ -830,7 +832,7 @@ private static {JsonParameterInfoValuesTypeRef}[] {typeGenerationSpec.TypeInfoPr
                     else
                     {
                         serializationLogic = $@"
-    {WriterVarName}.WritePropertyName({propName});
+    {WriterVarName}.WritePropertyName({propVarName});
     {GetSerializeLogicForNonPrimitiveType(propertyTypeSpec.TypeInfoPropertyName, propValue, propertyTypeSpec.GenerateSerializationLogic)}";
                     }
 
@@ -1190,10 +1192,10 @@ private static {JsonSerializerOptionsTypeRef} {DefaultOptionsStaticVarName} {{ g
 
                 StringBuilder sb = new();
 
-                foreach (string propName in _currentContext.RuntimePropertyNames)
+                foreach (KeyValuePair<string, string> name_varName_pair in _currentContext.RuntimePropertyNames)
                 {
                     sb.Append($@"
-private static {JsonEncodedTextTypeRef} {propName}PropName = {JsonEncodedTextTypeRef}.Encode(""{propName}"");");
+private static readonly {JsonEncodedTextTypeRef} {name_varName_pair.Value} = {JsonEncodedTextTypeRef}.Encode(""{name_varName_pair.Key}"");");
                 }
 
                 return sb.ToString();
index 33a6056..b75b7a6 100644 (file)
@@ -1014,6 +1014,8 @@ namespace System.Text.Json.SourceGeneration
                 }
 
                 string clrName = memberInfo.Name;
+                string runtimePropertyName = DetermineRuntimePropName(clrName, jsonPropertyName, _currentContextNamingPolicy);
+                string propertyNameVarName = DeterminePropNameIdentifier(runtimePropertyName);
 
                 return new PropertyGenerationSpec
                 {
@@ -1022,7 +1024,8 @@ namespace System.Text.Json.SourceGeneration
                     IsPublic = isPublic,
                     IsVirtual = isVirtual,
                     JsonPropertyName = jsonPropertyName,
-                    RuntimePropertyName = DetermineRuntimePropName(clrName, jsonPropertyName, _currentContextNamingPolicy),
+                    RuntimePropertyName = runtimePropertyName,
+                    PropertyNameVarName = propertyNameVarName,
                     IsReadOnly = isReadOnly,
                     CanUseGetter = canUseGetter,
                     CanUseSetter = canUseSetter,
@@ -1079,6 +1082,37 @@ namespace System.Text.Json.SourceGeneration
                 return runtimePropName;
             }
 
+            private static string DeterminePropNameIdentifier(string runtimePropName)
+            {
+                const string PropName = "PropName_";
+
+                // Use a different prefix to avoid possible collisions with "PropName_" in
+                // the rare case there is a C# property in a hex format.
+                const string EncodedPropName = "EncodedPropName_";
+
+                if (SyntaxFacts.IsValidIdentifier(runtimePropName))
+                {
+                    return PropName + runtimePropName;
+                }
+
+                // Encode the string to a byte[] and then convert to hexadecimal.
+                // To make the generated code more readable, we could use a different strategy in the future
+                // such as including the full class name + the CLR property name when there are duplicates,
+                // but that will create unnecessary JsonEncodedText properties.
+                byte[] utf8Json = Encoding.UTF8.GetBytes(runtimePropName);
+
+                StringBuilder sb = new StringBuilder(
+                    EncodedPropName,
+                    capacity: EncodedPropName.Length + utf8Json.Length * 2);
+
+                for (int i = 0; i < utf8Json.Length; i++)
+                {
+                    sb.Append(utf8Json[i].ToString("X2")); // X2 is hex format
+                }
+
+                return sb.ToString();
+            }
+
             private void PopulateNumberTypes()
             {
                 Debug.Assert(_numberTypes != null);
index 9d05fdf..74740cc 100644 (file)
@@ -32,6 +32,8 @@ namespace System.Text.Json.SourceGeneration
         /// </summary>
         public string RuntimePropertyName { get; init; }
 
+        public string PropertyNameVarName { get; init; }
+
         /// <summary>
         /// Whether the property has a set method.
         /// </summary>
index b539d6d..4c4b4e0 100644 (file)
@@ -372,7 +372,7 @@ namespace System.Text.Json
                     else if (currentByte == 'u')
                     {
                         // The source is known to be valid JSON, and hence if we see a \u, it is guaranteed to have 4 hex digits following it
-                        // Otherwise, the Utf8JsonReader would have alreayd thrown an exception.
+                        // Otherwise, the Utf8JsonReader would have already thrown an exception.
                         Debug.Assert(source.Length >= idx + 5);
 
                         bool result = Utf8Parser.TryParse(source.Slice(idx + 1, 4), out int scalar, out int bytesConsumed, 'x');
@@ -399,7 +399,7 @@ namespace System.Text.Json
                             }
 
                             // The source is known to be valid JSON, and hence if we see a \u, it is guaranteed to have 4 hex digits following it
-                            // Otherwise, the Utf8JsonReader would have alreayd thrown an exception.
+                            // Otherwise, the Utf8JsonReader would have already thrown an exception.
                             result = Utf8Parser.TryParse(source.Slice(idx, 4), out int lowSurrogate, out bytesConsumed, 'x');
                             Debug.Assert(result);
                             Debug.Assert(bytesConsumed == 4);
diff --git a/src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs b/src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs
new file mode 100644 (file)
index 0000000..fe1c4b5
--- /dev/null
@@ -0,0 +1,484 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// This file is saved as Unicode in order to test inline (not escaped) unicode characters.
+
+using System.Collections.Generic;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+    public abstract partial class PropertyNameTests : SerializerTests
+    {
+        public PropertyNameTests(JsonSerializerWrapperForString serializerWrapper) : base(serializerWrapper) { }
+
+        [Fact]
+        public async Task CamelCaseDeserializeNoMatch()
+        {
+            var options = new JsonSerializerOptions();
+            options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+
+            SimpleTestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper<SimpleTestClass>(@"{""MyInt16"":1}", options);
+
+            // This is 0 (default value) because the data does not match the property "MyInt16" that is assuming camel-casing of "myInt16".
+            Assert.Equal(0, obj.MyInt16);
+        }
+
+        [Fact]
+        public async Task CamelCaseDeserializeMatch()
+        {
+            var options = new JsonSerializerOptions();
+            options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+
+            SimpleTestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper<SimpleTestClass>(@"{""myInt16"":1}", options);
+
+            // This is 1 because the data matches the property "MyInt16" that is assuming camel-casing of "myInt16".
+            Assert.Equal(1, obj.MyInt16);
+        }
+
+        [Fact]
+        public async Task CamelCaseSerialize()
+        {
+            var options = new JsonSerializerOptions();
+            options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+
+            SimpleTestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper<SimpleTestClass>(@"{}", options);
+
+            string json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options);
+            Assert.Contains(@"""myInt16"":0", json);
+            Assert.Contains(@"""myInt32"":0", json);
+        }
+
+        [Fact]
+        public async Task CustomNamePolicy()
+        {
+            var options = new JsonSerializerOptions();
+            options.PropertyNamingPolicy = new UppercaseNamingPolicy();
+
+            SimpleTestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper<SimpleTestClass>(@"{""MYINT16"":1}", options);
+
+            // This is 1 because the data matches the property "MYINT16" that is uppercase of "myInt16".
+            Assert.Equal(1, obj.MyInt16);
+        }
+
+        [Fact]
+        public async Task NullNamePolicy()
+        {
+            var options = new JsonSerializerOptions();
+            options.PropertyNamingPolicy = new NullNamingPolicy();
+
+            // A policy that returns null is not allowed.
+            await Assert.ThrowsAsync<InvalidOperationException>(async () => await JsonSerializerWrapperForString.DeserializeWrapper<SimpleTestClass>(@"{}", options));
+            await Assert.ThrowsAsync<InvalidOperationException>(async () => await JsonSerializerWrapperForString.SerializeWrapper(new SimpleTestClass(), options));
+        }
+
+        [Fact]
+        public async Task IgnoreCase()
+        {
+            {
+                // A non-match scenario with no options (case-sensitive by default).
+                SimpleTestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper<SimpleTestClass>(@"{""myint16"":1}");
+                Assert.Equal(0, obj.MyInt16);
+            }
+
+            {
+                // A non-match scenario with default options (case-sensitive by default).
+                var options = new JsonSerializerOptions();
+                SimpleTestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper<SimpleTestClass>(@"{""myint16"":1}", options);
+                Assert.Equal(0, obj.MyInt16);
+            }
+
+            {
+                var options = new JsonSerializerOptions();
+                options.PropertyNameCaseInsensitive = true;
+                SimpleTestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper<SimpleTestClass>(@"{""myint16"":1}", options);
+                Assert.Equal(1, obj.MyInt16);
+            }
+        }
+
+        [Fact]
+        public async Task JsonPropertyNameAttribute()
+        {
+            {
+                OverridePropertyNameDesignTime_TestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper<OverridePropertyNameDesignTime_TestClass>(@"{""Blah"":1}");
+                Assert.Equal(1, obj.myInt);
+
+                obj.myObject = 2;
+
+                string json = await JsonSerializerWrapperForString.SerializeWrapper(obj);
+                Assert.Contains(@"""Blah"":1", json);
+                Assert.Contains(@"""BlahObject"":2", json);
+            }
+
+            // The JsonPropertyNameAttribute should be unaffected by JsonNamingPolicy and PropertyNameCaseInsensitive.
+            {
+                var options = new JsonSerializerOptions();
+                options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+                options.PropertyNameCaseInsensitive = true;
+
+                OverridePropertyNameDesignTime_TestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper<OverridePropertyNameDesignTime_TestClass>(@"{""Blah"":1}", options);
+                Assert.Equal(1, obj.myInt);
+
+                string json = await JsonSerializerWrapperForString.SerializeWrapper(obj);
+                Assert.Contains(@"""Blah"":1", json);
+            }
+        }
+
+        [Fact]
+        public async Task JsonNameAttributeDuplicateDesignTimeFail()
+        {
+            {
+                var options = new JsonSerializerOptions();
+                await Assert.ThrowsAsync<InvalidOperationException>(async () => await JsonSerializerWrapperForString.DeserializeWrapper<DuplicatePropertyNameDesignTime_TestClass>("{}", options));
+            }
+
+            {
+                var options = new JsonSerializerOptions();
+                await Assert.ThrowsAsync<InvalidOperationException>(async () => await JsonSerializerWrapperForString.SerializeWrapper(new DuplicatePropertyNameDesignTime_TestClass(), options));
+            }
+        }
+
+        [Fact]
+        public async Task JsonNameConflictOnCamelCasingFail()
+        {
+            {
+                // Baseline comparison - no options set.
+                IntPropertyNamesDifferentByCaseOnly_TestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper<IntPropertyNamesDifferentByCaseOnly_TestClass>("{}");
+                await JsonSerializerWrapperForString.SerializeWrapper(obj);
+            }
+
+            {
+                var options = new JsonSerializerOptions();
+                options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+
+                await Assert.ThrowsAsync<InvalidOperationException>(async () => await JsonSerializerWrapperForString.DeserializeWrapper<IntPropertyNamesDifferentByCaseOnly_TestClass>("{}", options));
+                await Assert.ThrowsAsync<InvalidOperationException>(async () => await JsonSerializerWrapperForString.SerializeWrapper(new IntPropertyNamesDifferentByCaseOnly_TestClass(), options));
+            }
+
+            {
+                // Baseline comparison - no options set.
+                ObjectPropertyNamesDifferentByCaseOnly_TestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper<ObjectPropertyNamesDifferentByCaseOnly_TestClass>("{}");
+                await JsonSerializerWrapperForString.SerializeWrapper(obj);
+            }
+
+            {
+                var options = new JsonSerializerOptions();
+                options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+
+                await Assert.ThrowsAsync<InvalidOperationException>(async () => await JsonSerializerWrapperForString.DeserializeWrapper<ObjectPropertyNamesDifferentByCaseOnly_TestClass>("{}", options));
+                await Assert.ThrowsAsync<InvalidOperationException>(async () => await JsonSerializerWrapperForString.SerializeWrapper(new ObjectPropertyNamesDifferentByCaseOnly_TestClass(), options));
+            }
+        }
+
+        [Fact]
+        public async Task JsonOutputNotAffectedByCasingPolicy()
+        {
+            {
+                // Baseline.
+                string json = await JsonSerializerWrapperForString.SerializeWrapper(new SimpleTestClass());
+                Assert.Contains(@"""MyInt16"":0", json);
+            }
+
+            // The JSON output should be unaffected by PropertyNameCaseInsensitive.
+            {
+                var options = new JsonSerializerOptions();
+                options.PropertyNameCaseInsensitive = true;
+
+                string json = await JsonSerializerWrapperForString.SerializeWrapper(new SimpleTestClass(), options);
+                Assert.Contains(@"""MyInt16"":0", json);
+            }
+        }
+
+        [Fact]
+        public async Task EmptyPropertyName()
+        {
+            string json = @"{"""":1}";
+
+            {
+                var obj = new EmptyPropertyName_TestClass();
+                obj.MyInt1 = 1;
+
+                string jsonOut = await JsonSerializerWrapperForString.SerializeWrapper(obj);
+                Assert.Equal(json, jsonOut);
+            }
+
+            {
+                EmptyPropertyName_TestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper<EmptyPropertyName_TestClass>(json);
+                Assert.Equal(1, obj.MyInt1);
+            }
+        }
+
+        [Fact]
+        public async Task UnicodePropertyNames()
+        {
+            ClassWithUnicodeProperty obj = await JsonSerializerWrapperForString.DeserializeWrapper<ClassWithUnicodeProperty>("{\"A\u0467\":1}");
+            Assert.Equal(1, obj.A\u0467);
+
+            // Specifying encoder on options does not impact deserialize.
+            var options = new JsonSerializerOptions();
+            options.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
+
+            obj = await JsonSerializerWrapperForString.DeserializeWrapper<ClassWithUnicodeProperty>("{\"A\u0467\":1}", options);
+            Assert.Equal(1, obj.A\u0467);
+
+            string json;
+
+            // Verify the name is escaped after serialize.
+            json = await JsonSerializerWrapperForString.SerializeWrapper(obj);
+            Assert.Contains(@"""A\u0467"":1", json);
+
+            // With custom escaper
+            json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options);
+            Assert.Contains("\"A\u0467\":1", json);
+
+            // Verify the name is unescaped after deserialize.
+            obj = await JsonSerializerWrapperForString.DeserializeWrapper<ClassWithUnicodeProperty>(json);
+            Assert.Equal(1, obj.A\u0467);
+
+            // With custom escaper
+            obj = await JsonSerializerWrapperForString.DeserializeWrapper<ClassWithUnicodeProperty>(json, options);
+            Assert.Equal(1, obj.A\u0467);
+        }
+
+        [Fact]
+        public async Task UnicodePropertyNamesWithPooledAlloc()
+        {
+            // We want to go over StackallocByteThreshold=256 to force a pooled allocation, so this property is 400 chars and 401 bytes.
+            ClassWithUnicodeProperty obj = await JsonSerializerWrapperForString.DeserializeWrapper<ClassWithUnicodeProperty>("{\"A\u046734567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890\":1}");
+            Assert.Equal(1, obj.A\u046734567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890);
+
+            // Verify the name is escaped after serialize.
+            string json = await JsonSerializerWrapperForString.SerializeWrapper(obj);
+            Assert.Contains(@"""A\u046734567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"":1", json);
+
+            // Verify the name is unescaped after deserialize.
+            obj = await JsonSerializerWrapperForString.DeserializeWrapper<ClassWithUnicodeProperty>(json);
+            Assert.Equal(1, obj.A\u046734567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890);
+        }
+
+        public class ClassWithPropertyNamePermutations
+        {
+            public int a { get; set; }
+            public int aa { get; set; }
+            public int aaa { get; set; }
+            public int aaaa { get; set; }
+            public int aaaaa { get; set; }
+            public int aaaaaa { get; set; }
+
+            // 7 characters - caching code only keys up to 7.
+            public int aaaaaaa { get; set; }
+            public int aaaaaab { get; set; }
+
+            // 8 characters.
+            public int aaaaaaaa { get; set; }
+            public int aaaaaaab { get; set; }
+
+            // 9 characters.
+            public int aaaaaaaaa { get; set; }
+            public int aaaaaaaab { get; set; }
+
+            public int \u0467 { get; set; }
+            public int \u0467\u0467 { get; set; }
+            public int \u0467\u0467a { get; set; }
+            public int \u0467\u0467b { get; set; }
+            public int \u0467\u0467\u0467 { get; set; }
+            public int \u0467\u0467\u0467a { get; set; }
+            public int \u0467\u0467\u0467b { get; set; }
+            public int \u0467\u0467\u0467\u0467 { get; set; }
+            public int \u0467\u0467\u0467\u0467a { get; set; }
+            public int \u0467\u0467\u0467\u0467b { get; set; }
+        }
+
+        [Fact]
+        public async Task CachingKeys()
+        {
+            ClassWithPropertyNamePermutations obj;
+
+            void Verify()
+            {
+                Assert.Equal(1, obj.a);
+                Assert.Equal(2, obj.aa);
+                Assert.Equal(3, obj.aaa);
+                Assert.Equal(4, obj.aaaa);
+                Assert.Equal(5, obj.aaaaa);
+                Assert.Equal(6, obj.aaaaaa);
+                Assert.Equal(7, obj.aaaaaaa);
+                Assert.Equal(7, obj.aaaaaab);
+                Assert.Equal(8, obj.aaaaaaaa);
+                Assert.Equal(8, obj.aaaaaaab);
+                Assert.Equal(9, obj.aaaaaaaaa);
+                Assert.Equal(9, obj.aaaaaaaab);
+
+                Assert.Equal(2, obj.\u0467);
+                Assert.Equal(4, obj.\u0467\u0467);
+                Assert.Equal(5, obj.\u0467\u0467a);
+                Assert.Equal(5, obj.\u0467\u0467b);
+                Assert.Equal(6, obj.\u0467\u0467\u0467);
+                Assert.Equal(7, obj.\u0467\u0467\u0467a);
+                Assert.Equal(7, obj.\u0467\u0467\u0467b);
+                Assert.Equal(8, obj.\u0467\u0467\u0467\u0467);
+                Assert.Equal(9, obj.\u0467\u0467\u0467\u0467a);
+                Assert.Equal(9, obj.\u0467\u0467\u0467\u0467b);
+            }
+
+            obj = new ClassWithPropertyNamePermutations
+            {
+                a = 1,
+                aa = 2,
+                aaa = 3,
+                aaaa = 4,
+                aaaaa = 5,
+                aaaaaa = 6,
+                aaaaaaa = 7,
+                aaaaaab = 7,
+                aaaaaaaa = 8,
+                aaaaaaab = 8,
+                aaaaaaaaa = 9,
+                aaaaaaaab = 9,
+                \u0467 = 2,
+                \u0467\u0467 = 4,
+                \u0467\u0467a = 5,
+                \u0467\u0467b = 5,
+                \u0467\u0467\u0467 = 6,
+                \u0467\u0467\u0467a = 7,
+                \u0467\u0467\u0467b = 7,
+                \u0467\u0467\u0467\u0467 = 8,
+                \u0467\u0467\u0467\u0467a = 9,
+                \u0467\u0467\u0467\u0467b = 9,
+            };
+
+            // Verify baseline.
+            Verify();
+
+            string json = await JsonSerializerWrapperForString.SerializeWrapper(obj);
+
+            // Verify the length is consistent with a verified value.
+            Assert.Equal(354, json.Length);
+
+            obj = await JsonSerializerWrapperForString.DeserializeWrapper<ClassWithPropertyNamePermutations>(json);
+
+            // Verify round-tripped object.
+            Verify();
+        }
+
+        [Fact]
+        public async Task BadNamingPolicy_ThrowsInvalidOperation()
+        {
+            var options = new JsonSerializerOptions { DictionaryKeyPolicy = new NullNamingPolicy() };
+
+            var inputPrimitive = new Dictionary<string, int>
+            {
+                { "validKey", 1 }
+            };
+
+            await Assert.ThrowsAsync<InvalidOperationException>(async () => await JsonSerializerWrapperForString.SerializeWrapper(inputPrimitive, options));
+
+            var inputClass = new Dictionary<string, OverridePropertyNameDesignTime_TestClass>
+            {
+                { "validKey", new OverridePropertyNameDesignTime_TestClass() }
+            };
+
+            await Assert.ThrowsAsync<InvalidOperationException>(async () => await JsonSerializerWrapperForString.SerializeWrapper(inputClass, options));
+        }
+
+        public class OverridePropertyNameDesignTime_TestClass
+        {
+            [JsonPropertyName("Blah")]
+            public int myInt { get; set; }
+
+            [JsonPropertyName("BlahObject")]
+            public object myObject { get; set; }
+        }
+
+        public class DuplicatePropertyNameDesignTime_TestClass
+        {
+            [JsonPropertyName("Blah")]
+            public int MyInt1 { get; set; }
+
+            [JsonPropertyName("Blah")]
+            public int MyInt2 { get; set; }
+        }
+
+        public class EmptyPropertyName_TestClass
+        {
+            [JsonPropertyName("")]
+            public int MyInt1 { get; set; }
+        }
+
+        public class NullPropertyName_TestClass
+        {
+            [JsonPropertyName(null)]
+            public int MyInt1 { get; set; }
+        }
+
+        public class IntPropertyNamesDifferentByCaseOnly_TestClass
+        {
+            public int myInt { get; set; }
+            public int MyInt { get; set; }
+        }
+
+        public class ObjectPropertyNamesDifferentByCaseOnly_TestClass
+        {
+            public int myObject { get; set; }
+            public int MyObject { get; set; }
+        }
+
+        [Fact]
+        public async Task SpecialCharacters()
+        {
+            ClassWithSpecialCharacters obj = new()
+            {
+                Baseline = 1,
+                Schema = 2,
+                SmtpId = 3,
+                Emojies = 4,
+                ꀀ = 5,
+                YiIt_2 = 6
+            };
+
+            string json = await JsonSerializerWrapperForString.SerializeWrapper(obj);
+            Assert.Equal(
+                "{\"Baseline\":1," +
+                "\"$schema\":2," +
+                "\"smtp-id\":3," +
+                "\"\\uD83D\\uDE00\\uD83D\\uDE01\":4," +
+                "\"\\uA000\":5," +
+                "\"\\uA000_2\":6}", json);
+
+            obj = await JsonSerializerWrapperForString.DeserializeWrapper<ClassWithSpecialCharacters>(json);
+            Assert.Equal(1, obj.Baseline);
+            Assert.Equal(2, obj.Schema);
+            Assert.Equal(3, obj.SmtpId);
+            Assert.Equal(4, obj.Emojies);
+            Assert.Equal(5, obj.ꀀ);
+            Assert.Equal(6, obj.YiIt_2);
+        }
+
+        public class ClassWithSpecialCharacters
+        {
+            [JsonPropertyOrder(1)]
+            public int Baseline { get; set; }
+
+            [JsonPropertyOrder(2)]
+            [JsonPropertyName("$schema")] // Invalid C# property name.
+            public int Schema { get; set; }
+
+            [JsonPropertyOrder(3)]
+            [JsonPropertyName("smtp-id")] // Invalid C# property name.
+            public int SmtpId { get; set; }
+
+            [JsonPropertyOrder(4)]
+            [JsonPropertyName("😀😁")] // Invalid C# property name. Unicode:\uD83D\uDE00\uD83D\uDE01
+            public int Emojies { get; set; }
+
+            [JsonPropertyOrder(5)]
+            public int ꀀ { get; set; } // Valid C# property name. Unicode:\uA000
+
+            [JsonPropertyOrder(6)]
+            [JsonPropertyName("\uA000_2")] // Valid C# property name: ꀀ_2
+            public int YiIt_2 { get; set; }
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyNameTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyNameTests.cs
new file mode 100644 (file)
index 0000000..1beb220
--- /dev/null
@@ -0,0 +1,60 @@
+// 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.Text.Json.Serialization;
+using System.Text.Json.Serialization.Tests;
+
+namespace System.Text.Json.SourceGeneration.Tests
+{
+    public sealed partial class PropertyNameTests_Metadata : PropertyNameTests
+    {
+        public PropertyNameTests_Metadata()
+            : base(new StringSerializerWrapper(PropertyNameTestsContext_Metadata.Default, (options) => new PropertyNameTestsContext_Metadata(options)))
+        {
+        }
+
+        [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
+        [JsonSerializable(typeof(Dictionary<string, OverridePropertyNameDesignTime_TestClass>))]
+        [JsonSerializable(typeof(Dictionary<string, int>))]
+        [JsonSerializable(typeof(int))]
+        [JsonSerializable(typeof(ClassWithSpecialCharacters))]
+        [JsonSerializable(typeof(ClassWithPropertyNamePermutations))]
+        [JsonSerializable(typeof(ClassWithUnicodeProperty))]
+        [JsonSerializable(typeof(DuplicatePropertyNameDesignTime_TestClass))]
+        [JsonSerializable(typeof(EmptyPropertyName_TestClass))]
+        [JsonSerializable(typeof(IntPropertyNamesDifferentByCaseOnly_TestClass))]
+        [JsonSerializable(typeof(NullPropertyName_TestClass))]
+        [JsonSerializable(typeof(ObjectPropertyNamesDifferentByCaseOnly_TestClass))]
+        [JsonSerializable(typeof(OverridePropertyNameDesignTime_TestClass))]
+        [JsonSerializable(typeof(SimpleTestClass))]
+        internal sealed partial class PropertyNameTestsContext_Metadata : JsonSerializerContext
+        {
+        }
+    }
+
+    public sealed partial class PropertyNameTests_Default : PropertyNameTests
+    {
+        public PropertyNameTests_Default()
+            : base(new StringSerializerWrapper(PropertyNameTestsContext_Default.Default, (options) => new PropertyNameTestsContext_Default(options)))
+        {
+        }
+
+        [JsonSerializable(typeof(Dictionary<string, OverridePropertyNameDesignTime_TestClass>))]
+        [JsonSerializable(typeof(Dictionary<string, int>))]
+        [JsonSerializable(typeof(int))]
+        [JsonSerializable(typeof(ClassWithSpecialCharacters))]
+        [JsonSerializable(typeof(ClassWithPropertyNamePermutations))]
+        [JsonSerializable(typeof(ClassWithUnicodeProperty))]
+        [JsonSerializable(typeof(DuplicatePropertyNameDesignTime_TestClass))]
+        [JsonSerializable(typeof(EmptyPropertyName_TestClass))]
+        [JsonSerializable(typeof(IntPropertyNamesDifferentByCaseOnly_TestClass))]
+        [JsonSerializable(typeof(NullPropertyName_TestClass))]
+        [JsonSerializable(typeof(ObjectPropertyNamesDifferentByCaseOnly_TestClass))]
+        [JsonSerializable(typeof(OverridePropertyNameDesignTime_TestClass))]
+        [JsonSerializable(typeof(SimpleTestClass))]
+        internal sealed partial class PropertyNameTestsContext_Default : JsonSerializerContext
+        {
+        }
+    }
+}
index 3fe38b9..e848e3e 100644 (file)
@@ -41,6 +41,7 @@
     <Compile Include="..\Common\JsonSerializerWrapperForStream.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\JsonSerializerWrapperForStream.cs" />
     <Compile Include="..\Common\JsonSerializerWrapperForString.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\JsonSerializerWrapperForString.cs" />
     <Compile Include="..\Common\JsonTestHelper.cs" Link="CommonTest\System\Text\Json\JsonTestHelper.cs" />
+    <Compile Include="..\Common\PropertyNameTests.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\PropertyNameTests.cs" />
     <Compile Include="..\Common\PropertyVisibilityTests.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\PropertyVisibilityTests.cs" />
     <Compile Include="..\Common\PropertyVisibilityTests.InitOnly.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\PropertyVisibilityTests.InitOnly.cs" />
     <Compile Include="..\Common\PropertyVisibilityTests.NonPublicAccessors.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\PropertyVisibilityTests.NonPublicAccessors.cs" />
@@ -72,6 +73,7 @@
     <Compile Include="MixedModeContextTests.cs" />
     <Compile Include="SerializationContextTests.cs" />
     <Compile Include="SerializationLogicTests.cs" />
+    <Compile Include="Serialization\PropertyNameTests.cs" />
     <Compile Include="Serialization\PropertyVisibilityTests.cs" />
     <Compile Include="TestClasses.cs" />
     <Compile Include="RealWorldContextTests.cs" />
index 853ec36..7a64f8e 100644 (file)
@@ -5,6 +5,7 @@ using System.Collections.Generic;
 using System.Collections.Immutable;
 using System.Linq;
 using System.Reflection;
+using System.Text.Encodings.Web;
 using System.Text.Json.Nodes;
 using System.Text.Json.Serialization;
 using Xunit;
@@ -14,6 +15,41 @@ namespace System.Text.Json.Serialization.Tests
     public static class ExtensionDataTests
     {
         [Fact]
+        public static void EmptyPropertyName_WinsOver_ExtensionDataEmptyPropertyName()
+        {
+            string json = @"{"""":1}";
+
+            ClassWithEmptyPropertyNameAndExtensionProperty obj;
+
+            // Create a new options instances to re-set any caches.
+            JsonSerializerOptions options = new JsonSerializerOptions();
+
+            // Verify the real property wins over the extension data property.
+            obj = JsonSerializer.Deserialize<ClassWithEmptyPropertyNameAndExtensionProperty>(json, options);
+            Assert.Equal(1, obj.MyInt1);
+            Assert.Null(obj.MyOverflow);
+        }
+
+        [Fact]
+        public static void EmptyPropertyNameInExtensionData()
+        {
+            {
+                string json = @"{"""":42}";
+                EmptyClassWithExtensionProperty obj = JsonSerializer.Deserialize<EmptyClassWithExtensionProperty>(json);
+                Assert.Equal(1, obj.MyOverflow.Count);
+                Assert.Equal(42, obj.MyOverflow[""].GetInt32());
+            }
+
+            {
+                // Verify that last-in wins.
+                string json = @"{"""":42, """":43}";
+                EmptyClassWithExtensionProperty obj = JsonSerializer.Deserialize<EmptyClassWithExtensionProperty>(json);
+                Assert.Equal(1, obj.MyOverflow.Count);
+                Assert.Equal(43, obj.MyOverflow[""].GetInt32());
+            }
+        }
+
+        [Fact]
         public static void ExtensionPropertyNotUsed()
         {
             string json = @"{""MyNestedClass"":" + SimpleTestClass.s_json + "}";
@@ -1295,5 +1331,115 @@ namespace System.Text.Json.Serialization.Tests
                 writer.WriteEndObject();
             }
         }
+
+        [Fact]
+        public static void EmptyPropertyAndExtensionData_PropertyFirst()
+        {
+            // Verify any caching treats real property (with empty name) differently than a missing property.
+
+            ClassWithEmptyPropertyNameAndExtensionProperty obj;
+
+            // Create a new options instances to re-set any caches.
+            JsonSerializerOptions options = new JsonSerializerOptions();
+
+            // First use an empty property.
+            string json = @"{"""":43}";
+            obj = JsonSerializer.Deserialize<ClassWithEmptyPropertyNameAndExtensionProperty>(json, options);
+            Assert.Equal(43, obj.MyInt1);
+            Assert.Null(obj.MyOverflow);
+
+            // Then populate cache with a missing property name.
+            json = @"{""DoesNotExist"":42}";
+            obj = JsonSerializer.Deserialize<ClassWithEmptyPropertyNameAndExtensionProperty>(json, options);
+            Assert.Equal(0, obj.MyInt1);
+            Assert.Equal(1, obj.MyOverflow.Count);
+            Assert.Equal(42, obj.MyOverflow["DoesNotExist"].GetInt32());
+        }
+
+        [Fact]
+        public static void EmptyPropertyNameAndExtensionData_ExtDataFirst()
+        {
+            // Verify any caching treats real property (with empty name) differently than a missing property.
+
+            ClassWithEmptyPropertyNameAndExtensionProperty obj;
+
+            // Create a new options instances to re-set any caches.
+            JsonSerializerOptions options = new JsonSerializerOptions();
+
+            // First populate cache with a missing property name.
+            string json = @"{""DoesNotExist"":42}";
+            obj = JsonSerializer.Deserialize<ClassWithEmptyPropertyNameAndExtensionProperty>(json, options);
+            Assert.Equal(0, obj.MyInt1);
+            Assert.Equal(1, obj.MyOverflow.Count);
+            Assert.Equal(42, obj.MyOverflow["DoesNotExist"].GetInt32());
+
+            // Then use an empty property.
+            json = @"{"""":43}";
+            obj = JsonSerializer.Deserialize<ClassWithEmptyPropertyNameAndExtensionProperty>(json, options);
+            Assert.Equal(43, obj.MyInt1);
+            Assert.Null(obj.MyOverflow);
+        }
+
+        [Fact]
+        public static void ExtensionDataDictionarySerialize_DoesNotHonor()
+        {
+            var options = new JsonSerializerOptions
+            {
+                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+            };
+
+            EmptyClassWithExtensionProperty obj = JsonSerializer.Deserialize<EmptyClassWithExtensionProperty>(@"{""Key1"": 1}", options);
+
+            // Ignore naming policy for extension data properties by default.
+            Assert.False(obj.MyOverflow.ContainsKey("key1"));
+            Assert.Equal(1, obj.MyOverflow["Key1"].GetInt32());
+        }
+
+        [Theory]
+        [InlineData(0x1, 'v')]
+        [InlineData(0x1, '\u0467')]
+        [InlineData(0x10, 'v')]
+        [InlineData(0x10, '\u0467')]
+        [InlineData(0x100, 'v')]
+        [InlineData(0x100, '\u0467')]
+        [InlineData(0x1000, 'v')]
+        [InlineData(0x1000, '\u0467')]
+        [InlineData(0x10000, 'v')]
+        [InlineData(0x10000, '\u0467')]
+        public static void LongPropertyNames(int propertyLength, char ch)
+        {
+            // Although the CLR may limit member length to 1023 bytes, the serializer doesn't have a hard limit.
+
+            string val = new string(ch, propertyLength);
+            string json = @"{""" + val + @""":1}";
+
+            EmptyClassWithExtensionProperty obj = JsonSerializer.Deserialize<EmptyClassWithExtensionProperty>(json);
+
+            Assert.True(obj.MyOverflow.ContainsKey(val));
+
+            var options = new JsonSerializerOptions
+            {
+                // Avoid escaping '\u0467'.
+                Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+            };
+
+            string jsonRoundTripped = JsonSerializer.Serialize(obj, options);
+            Assert.Equal(json, jsonRoundTripped);
+        }
+
+        public class EmptyClassWithExtensionProperty
+        {
+            [JsonExtensionData]
+            public IDictionary<string, JsonElement> MyOverflow { get; set; }
+        }
+
+        public class ClassWithEmptyPropertyNameAndExtensionProperty
+        {
+            [JsonPropertyName("")]
+            public int MyInt1 { get; set; }
+
+            [JsonExtensionData]
+            public IDictionary<string, JsonElement> MyOverflow { get; set; }
+        }
     }
 }
index 6a14f3e..1423174 100644 (file)
-// Licensed to the .NET Foundation under one or more agreements.
+// 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.Text.Encodings.Web;
+using System.Threading.Tasks;
 using Xunit;
 
 namespace System.Text.Json.Serialization.Tests
 {
-    public static class PropertyNameTests
+    public sealed partial class PropertyNameTestsDynamic : PropertyNameTests
     {
-        [Fact]
-        public static void CamelCaseDeserializeNoMatch()
-        {
-            var options = new JsonSerializerOptions();
-            options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
-
-            SimpleTestClass obj = JsonSerializer.Deserialize<SimpleTestClass>(@"{""MyInt16"":1}", options);
-
-            // This is 0 (default value) because the data does not match the property "MyInt16" that is assuming camel-casing of "myInt16".
-            Assert.Equal(0, obj.MyInt16);
-        }
-
-        [Fact]
-        public static void CamelCaseDeserializeMatch()
-        {
-            var options = new JsonSerializerOptions();
-            options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
-
-            SimpleTestClass obj = JsonSerializer.Deserialize<SimpleTestClass>(@"{""myInt16"":1}", options);
-
-            // This is 1 because the data matches the property "MyInt16" that is assuming camel-casing of "myInt16".
-            Assert.Equal(1, obj.MyInt16);
-        }
-
-        [Fact]
-        public static void CamelCaseSerialize()
-        {
-            var options = new JsonSerializerOptions();
-            options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
-
-            SimpleTestClass obj = JsonSerializer.Deserialize<SimpleTestClass>(@"{}", options);
-
-            string json = JsonSerializer.Serialize(obj, options);
-            Assert.Contains(@"""myInt16"":0", json);
-            Assert.Contains(@"""myInt32"":0", json);
-        }
-
-        [Fact]
-        public static void CustomNamePolicy()
-        {
-            var options = new JsonSerializerOptions();
-            options.PropertyNamingPolicy = new UppercaseNamingPolicy();
-
-            SimpleTestClass obj = JsonSerializer.Deserialize<SimpleTestClass>(@"{""MYINT16"":1}", options);
-
-            // This is 1 because the data matches the property "MYINT16" that is uppercase of "myInt16".
-            Assert.Equal(1, obj.MyInt16);
-        }
-
-        [Fact]
-        public static void NullNamePolicy()
-        {
-            var options = new JsonSerializerOptions();
-            options.PropertyNamingPolicy = new NullNamingPolicy();
-
-            // A policy that returns null is not allowed.
-            Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<SimpleTestClass>(@"{}", options));
-            Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new SimpleTestClass(), options));
-        }
-
-        [Fact]
-        public static void IgnoreCase()
-        {
-            {
-                // A non-match scenario with no options (case-sensitive by default).
-                SimpleTestClass obj = JsonSerializer.Deserialize<SimpleTestClass>(@"{""myint16"":1}");
-                Assert.Equal(0, obj.MyInt16);
-            }
-
-            {
-                // A non-match scenario with default options (case-sensitive by default).
-                var options = new JsonSerializerOptions();
-                SimpleTestClass obj = JsonSerializer.Deserialize<SimpleTestClass>(@"{""myint16"":1}", options);
-                Assert.Equal(0, obj.MyInt16);
-            }
-
-            {
-                var options = new JsonSerializerOptions();
-                options.PropertyNameCaseInsensitive = true;
-                SimpleTestClass obj = JsonSerializer.Deserialize<SimpleTestClass>(@"{""myint16"":1}", options);
-                Assert.Equal(1, obj.MyInt16);
-            }
-        }
-
-        [Fact]
-        public static void JsonPropertyNameAttribute()
-        {
-            {
-                OverridePropertyNameDesignTime_TestClass obj = JsonSerializer.Deserialize<OverridePropertyNameDesignTime_TestClass>(@"{""Blah"":1}");
-                Assert.Equal(1, obj.myInt);
-
-                obj.myObject = 2;
-
-                string json = JsonSerializer.Serialize(obj);
-                Assert.Contains(@"""Blah"":1", json);
-                Assert.Contains(@"""BlahObject"":2", json);
-            }
-
-            // The JsonPropertyNameAttribute should be unaffected by JsonNamingPolicy and PropertyNameCaseInsensitive.
-            {
-                var options = new JsonSerializerOptions();
-                options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
-                options.PropertyNameCaseInsensitive = true;
-
-                OverridePropertyNameDesignTime_TestClass obj = JsonSerializer.Deserialize<OverridePropertyNameDesignTime_TestClass>(@"{""Blah"":1}", options);
-                Assert.Equal(1, obj.myInt);
-
-                string json = JsonSerializer.Serialize(obj);
-                Assert.Contains(@"""Blah"":1", json);
-            }
-        }
-
-        [Fact]
-        public static void JsonNameAttributeDuplicateDesignTimeFail()
-        {
-            {
-                var options = new JsonSerializerOptions();
-                Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<DuplicatePropertyNameDesignTime_TestClass>("{}", options));
-            }
-
-            {
-                var options = new JsonSerializerOptions();
-                Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new DuplicatePropertyNameDesignTime_TestClass(), options));
-            }
-        }
+        public PropertyNameTestsDynamic() : base(JsonSerializerWrapperForString.StringSerializer) { }
 
         [Fact]
-        public static void JsonNullNameAttribute()
+        public async Task JsonNullNameAttribute()
         {
             var options = new JsonSerializerOptions();
             options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
             options.PropertyNameCaseInsensitive = true;
 
             // A null name in JsonPropertyNameAttribute is not allowed.
-            Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new NullPropertyName_TestClass(), options));
+            await Assert.ThrowsAsync<InvalidOperationException>(async () => await JsonSerializerWrapperForString.SerializeWrapper(new NullPropertyName_TestClass(), options));
         }
 
         [Fact]
-        public static void JsonNameConflictOnCamelCasingFail()
-        {
-            {
-                // Baseline comparison - no options set.
-                IntPropertyNamesDifferentByCaseOnly_TestClass obj = JsonSerializer.Deserialize<IntPropertyNamesDifferentByCaseOnly_TestClass>("{}");
-                JsonSerializer.Serialize(obj);
-            }
-
-            {
-                var options = new JsonSerializerOptions();
-                options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
-
-                Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<IntPropertyNamesDifferentByCaseOnly_TestClass>("{}", options));
-                Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new IntPropertyNamesDifferentByCaseOnly_TestClass(), options));
-            }
-
-            {
-                // Baseline comparison - no options set.
-                ObjectPropertyNamesDifferentByCaseOnly_TestClass obj = JsonSerializer.Deserialize<ObjectPropertyNamesDifferentByCaseOnly_TestClass>("{}");
-                JsonSerializer.Serialize(obj);
-            }
-
-            {
-                var options = new JsonSerializerOptions();
-                options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
-
-                Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<ObjectPropertyNamesDifferentByCaseOnly_TestClass>("{}", options));
-                Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new ObjectPropertyNamesDifferentByCaseOnly_TestClass(), options));
-            }
-        }
-
-        [Fact]
-        public static void JsonNameConflictOnCaseInsensitiveFail()
+        public async Task JsonNameConflictOnCaseInsensitiveFail()
         {
             string json = @"{""myInt"":1,""MyInt"":2}";
 
@@ -187,407 +30,9 @@ namespace System.Text.Json.Serialization.Tests
                 var options = new JsonSerializerOptions();
                 options.PropertyNameCaseInsensitive = true;
 
-                Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<IntPropertyNamesDifferentByCaseOnly_TestClass>(json, options));
-                Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new IntPropertyNamesDifferentByCaseOnly_TestClass(), options));
-            }
-        }
-
-        [Fact]
-        public static void JsonOutputNotAffectedByCasingPolicy()
-        {
-            {
-                // Baseline.
-                string json = JsonSerializer.Serialize(new SimpleTestClass());
-                Assert.Contains(@"""MyInt16"":0", json);
-            }
-
-            // The JSON output should be unaffected by PropertyNameCaseInsensitive.
-            {
-                var options = new JsonSerializerOptions();
-                options.PropertyNameCaseInsensitive = true;
-
-                string json = JsonSerializer.Serialize(new SimpleTestClass(), options);
-                Assert.Contains(@"""MyInt16"":0", json);
-            }
-        }
-
-        [Fact]
-        public static void EmptyPropertyName()
-        {
-            string json = @"{"""":1}";
-
-            {
-                var obj = new EmptyPropertyName_TestClass();
-                obj.MyInt1 = 1;
-
-                string jsonOut = JsonSerializer.Serialize(obj);
-                Assert.Equal(json, jsonOut);
-            }
-
-            {
-                EmptyPropertyName_TestClass obj = JsonSerializer.Deserialize<EmptyPropertyName_TestClass>(json);
-                Assert.Equal(1, obj.MyInt1);
-            }
-        }
-
-        [Fact]
-        public static void EmptyPropertyNameInExtensionData()
-        {
-            {
-                string json = @"{"""":42}";
-                EmptyClassWithExtensionProperty obj = JsonSerializer.Deserialize<EmptyClassWithExtensionProperty>(json);
-                Assert.Equal(1, obj.MyOverflow.Count);
-                Assert.Equal(42, obj.MyOverflow[""].GetInt32());
-            }
-
-            {
-                // Verify that last-in wins.
-                string json = @"{"""":42, """":43}";
-                EmptyClassWithExtensionProperty obj = JsonSerializer.Deserialize<EmptyClassWithExtensionProperty>(json);
-                Assert.Equal(1, obj.MyOverflow.Count);
-                Assert.Equal(43, obj.MyOverflow[""].GetInt32());
-            }
-        }
-
-        [Fact]
-        public static void EmptyPropertyName_WinsOver_ExtensionDataEmptyPropertyName()
-        {
-            string json = @"{"""":1}";
-
-            ClassWithEmptyPropertyNameAndExtensionProperty obj;
-
-            // Create a new options instances to re-set any caches.
-            JsonSerializerOptions options = new JsonSerializerOptions();
-
-            // Verify the real property wins over the extension data property.
-            obj = JsonSerializer.Deserialize<ClassWithEmptyPropertyNameAndExtensionProperty>(json, options);
-            Assert.Equal(1, obj.MyInt1);
-            Assert.Null(obj.MyOverflow);
-        }
-
-        [Fact]
-        public static void EmptyPropertyNameAndExtensionData_ExtDataFirst()
-        {
-            // Verify any caching treats real property (with empty name) differently than a missing property.
-
-            ClassWithEmptyPropertyNameAndExtensionProperty obj;
-
-            // Create a new options instances to re-set any caches.
-            JsonSerializerOptions options = new JsonSerializerOptions();
-
-            // First populate cache with a missing property name.
-            string json = @"{""DoesNotExist"":42}";
-            obj = JsonSerializer.Deserialize<ClassWithEmptyPropertyNameAndExtensionProperty>(json, options);
-            Assert.Equal(0, obj.MyInt1);
-            Assert.Equal(1, obj.MyOverflow.Count);
-            Assert.Equal(42, obj.MyOverflow["DoesNotExist"].GetInt32());
-
-            // Then use an empty property.
-            json = @"{"""":43}";
-            obj = JsonSerializer.Deserialize<ClassWithEmptyPropertyNameAndExtensionProperty>(json, options);
-            Assert.Equal(43, obj.MyInt1);
-            Assert.Null(obj.MyOverflow);
-        }
-
-        [Fact]
-        public static void EmptyPropertyAndExtensionData_PropertyFirst()
-        {
-            // Verify any caching treats real property (with empty name) differently than a missing property.
-
-            ClassWithEmptyPropertyNameAndExtensionProperty obj;
-
-            // Create a new options instances to re-set any caches.
-            JsonSerializerOptions options = new JsonSerializerOptions();
-
-            // First use an empty property.
-            string json = @"{"""":43}";
-            obj = JsonSerializer.Deserialize<ClassWithEmptyPropertyNameAndExtensionProperty>(json, options);
-            Assert.Equal(43, obj.MyInt1);
-            Assert.Null(obj.MyOverflow);
-
-            // Then populate cache with a missing property name.
-            json = @"{""DoesNotExist"":42}";
-            obj = JsonSerializer.Deserialize<ClassWithEmptyPropertyNameAndExtensionProperty>(json, options);
-            Assert.Equal(0, obj.MyInt1);
-            Assert.Equal(1, obj.MyOverflow.Count);
-            Assert.Equal(42, obj.MyOverflow["DoesNotExist"].GetInt32());
-        }
-
-        [Fact]
-        public static void UnicodePropertyNames()
-        {
-            ClassWithUnicodeProperty obj = JsonSerializer.Deserialize<ClassWithUnicodeProperty>("{\"A\u0467\":1}");
-            Assert.Equal(1, obj.A\u0467);
-
-            // Specifying encoder on options does not impact deserialize.
-            var options = new JsonSerializerOptions();
-            options.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
-
-            obj = JsonSerializer.Deserialize<ClassWithUnicodeProperty>("{\"A\u0467\":1}", options);
-            Assert.Equal(1, obj.A\u0467);
-
-            string json;
-
-            // Verify the name is escaped after serialize.
-            json = JsonSerializer.Serialize(obj);
-            Assert.Contains(@"""A\u0467"":1", json);
-
-            // With custom escaper
-            json = JsonSerializer.Serialize(obj, options);
-            Assert.Contains("\"A\u0467\":1", json);
-
-            // Verify the name is unescaped after deserialize.
-            obj = JsonSerializer.Deserialize<ClassWithUnicodeProperty>(json);
-            Assert.Equal(1, obj.A\u0467);
-
-            // With custom escaper
-            obj = JsonSerializer.Deserialize<ClassWithUnicodeProperty>(json, options);
-            Assert.Equal(1, obj.A\u0467);
-        }
-
-        [Fact]
-        public static void UnicodePropertyNamesWithPooledAlloc()
-        {
-            // We want to go over StackallocByteThreshold=256 to force a pooled allocation, so this property is 400 chars and 401 bytes.
-            ClassWithUnicodeProperty obj = JsonSerializer.Deserialize<ClassWithUnicodeProperty>("{\"A\u046734567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890\":1}");
-            Assert.Equal(1, obj.A\u046734567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890);
-
-            // Verify the name is escaped after serialize.
-            string json = JsonSerializer.Serialize(obj);
-            Assert.Contains(@"""A\u046734567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"":1", json);
-
-            // Verify the name is unescaped after deserialize.
-            obj = JsonSerializer.Deserialize<ClassWithUnicodeProperty>(json);
-            Assert.Equal(1, obj.A\u046734567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890);
-        }
-
-        [Fact]
-        public static void ExtensionDataDictionarySerialize_DoesNotHonor()
-        {
-            var options = new JsonSerializerOptions
-            {
-                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
-            };
-
-            EmptyClassWithExtensionProperty obj = JsonSerializer.Deserialize<EmptyClassWithExtensionProperty>(@"{""Key1"": 1}", options);
-
-            // Ignore naming policy for extension data properties by default.
-            Assert.False(obj.MyOverflow.ContainsKey("key1"));
-            Assert.Equal(1, obj.MyOverflow["Key1"].GetInt32());
-        }
-
-        private class ClassWithPropertyNamePermutations
-        {
-            public int a { get; set; }
-            public int aa { get; set; }
-            public int aaa { get; set; }
-            public int aaaa { get; set; }
-            public int aaaaa { get; set; }
-            public int aaaaaa { get; set; }
-
-            // 7 characters - caching code only keys up to 7.
-            public int aaaaaaa { get; set; }
-            public int aaaaaab { get; set; }
-
-            // 8 characters.
-            public int aaaaaaaa { get; set; }
-            public int aaaaaaab { get; set; }
-
-            // 9 characters.
-            public int aaaaaaaaa { get; set; }
-            public int aaaaaaaab { get; set; }
-
-            public int \u0467 { get; set; }
-            public int \u0467\u0467 { get; set; }
-            public int \u0467\u0467a { get; set; }
-            public int \u0467\u0467b { get; set; }
-            public int \u0467\u0467\u0467 { get; set; }
-            public int \u0467\u0467\u0467a { get; set; }
-            public int \u0467\u0467\u0467b { get; set; }
-            public int \u0467\u0467\u0467\u0467 { get; set; }
-            public int \u0467\u0467\u0467\u0467a { get; set; }
-            public int \u0467\u0467\u0467\u0467b { get; set; }
-        }
-
-        [Fact]
-        public static void CachingKeys()
-        {
-            ClassWithPropertyNamePermutations obj;
-
-            void Verify()
-            {
-                Assert.Equal(1, obj.a);
-                Assert.Equal(2, obj.aa);
-                Assert.Equal(3, obj.aaa);
-                Assert.Equal(4, obj.aaaa);
-                Assert.Equal(5, obj.aaaaa);
-                Assert.Equal(6, obj.aaaaaa);
-                Assert.Equal(7, obj.aaaaaaa);
-                Assert.Equal(7, obj.aaaaaab);
-                Assert.Equal(8, obj.aaaaaaaa);
-                Assert.Equal(8, obj.aaaaaaab);
-                Assert.Equal(9, obj.aaaaaaaaa);
-                Assert.Equal(9, obj.aaaaaaaab);
-
-                Assert.Equal(2, obj.\u0467);
-                Assert.Equal(4, obj.\u0467\u0467);
-                Assert.Equal(5, obj.\u0467\u0467a);
-                Assert.Equal(5, obj.\u0467\u0467b);
-                Assert.Equal(6, obj.\u0467\u0467\u0467);
-                Assert.Equal(7, obj.\u0467\u0467\u0467a);
-                Assert.Equal(7, obj.\u0467\u0467\u0467b);
-                Assert.Equal(8, obj.\u0467\u0467\u0467\u0467);
-                Assert.Equal(9, obj.\u0467\u0467\u0467\u0467a);
-                Assert.Equal(9, obj.\u0467\u0467\u0467\u0467b);
+                await Assert.ThrowsAsync<InvalidOperationException>(async () => await JsonSerializerWrapperForString.DeserializeWrapper<IntPropertyNamesDifferentByCaseOnly_TestClass>(json, options));
+                await Assert.ThrowsAsync<InvalidOperationException>(async () => await JsonSerializerWrapperForString.SerializeWrapper(new IntPropertyNamesDifferentByCaseOnly_TestClass(), options));
             }
-
-            obj = new ClassWithPropertyNamePermutations
-            {
-                a = 1,
-                aa = 2,
-                aaa = 3,
-                aaaa = 4,
-                aaaaa = 5,
-                aaaaaa = 6,
-                aaaaaaa = 7,
-                aaaaaab = 7,
-                aaaaaaaa = 8,
-                aaaaaaab = 8,
-                aaaaaaaaa = 9,
-                aaaaaaaab = 9,
-                \u0467 = 2,
-                \u0467\u0467 = 4,
-                \u0467\u0467a = 5,
-                \u0467\u0467b = 5,
-                \u0467\u0467\u0467 = 6,
-                \u0467\u0467\u0467a = 7,
-                \u0467\u0467\u0467b = 7,
-                \u0467\u0467\u0467\u0467 = 8,
-                \u0467\u0467\u0467\u0467a = 9,
-                \u0467\u0467\u0467\u0467b = 9,
-            };
-
-            // Verify baseline.
-            Verify();
-
-            string json = JsonSerializer.Serialize(obj);
-
-            // Verify the length is consistent with a verified value.
-            Assert.Equal(354, json.Length);
-
-            obj = JsonSerializer.Deserialize<ClassWithPropertyNamePermutations>(json);
-
-            // Verify round-tripped object.
-            Verify();
         }
-
-        [Theory]
-        [InlineData(0x1, 'v')]
-        [InlineData(0x1, '\u0467')]
-        [InlineData(0x10, 'v')]
-        [InlineData(0x10, '\u0467')]
-        [InlineData(0x100, 'v')]
-        [InlineData(0x100, '\u0467')]
-        [InlineData(0x1000, 'v')]
-        [InlineData(0x1000, '\u0467')]
-        [InlineData(0x10000, 'v')]
-        [InlineData(0x10000, '\u0467')]
-        public static void LongPropertyNames(int propertyLength, char ch)
-        {
-            // Although the CLR may limit member length to 1023 bytes, the serializer doesn't have a hard limit.
-
-            string val = new string(ch, propertyLength);
-            string json = @"{""" + val + @""":1}";
-
-            EmptyClassWithExtensionProperty obj = JsonSerializer.Deserialize<EmptyClassWithExtensionProperty>(json);
-
-            Assert.True(obj.MyOverflow.ContainsKey(val));
-
-            var options = new JsonSerializerOptions
-            {
-                // Avoid escaping '\u0467'.
-                Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
-            };
-
-            string jsonRoundTripped = JsonSerializer.Serialize(obj, options);
-            Assert.Equal(json, jsonRoundTripped);
-        }
-
-        [Fact]
-        public static void BadNamingPolicy_ThrowsInvalidOperation()
-        {
-            var options = new JsonSerializerOptions { DictionaryKeyPolicy = new NullNamingPolicy() };
-
-            var inputPrimitive = new Dictionary<string, int>
-            {
-                { "validKey", 1 }
-            };
-
-            Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(inputPrimitive, options));
-
-            var inputClass = new Dictionary<string, OverridePropertyNameDesignTime_TestClass>
-            {
-                { "validKey", new OverridePropertyNameDesignTime_TestClass() }
-            };
-
-            Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(inputClass, options));
-        }
-    }
-
-    public class OverridePropertyNameDesignTime_TestClass
-    {
-        [JsonPropertyName("Blah")]
-        public int myInt { get; set; }
-
-        [JsonPropertyName("BlahObject")]
-        public object myObject { get; set; }
-    }
-
-    public class DuplicatePropertyNameDesignTime_TestClass
-    {
-        [JsonPropertyName("Blah")]
-        public int MyInt1 { get; set; }
-
-        [JsonPropertyName("Blah")]
-        public int MyInt2 { get; set; }
-    }
-
-    public class EmptyPropertyName_TestClass
-    {
-        [JsonPropertyName("")]
-        public int MyInt1 { get; set; }
-    }
-
-    public class NullPropertyName_TestClass
-    {
-        [JsonPropertyName(null)]
-        public int MyInt1 { get; set; }
-    }
-
-    public class IntPropertyNamesDifferentByCaseOnly_TestClass
-    {
-        public int myInt { get; set; }
-        public int MyInt { get; set; }
-    }
-
-    public class ObjectPropertyNamesDifferentByCaseOnly_TestClass
-    {
-        public int myObject { get; set; }
-        public int MyObject { get; set; }
-    }
-
-    public class EmptyClassWithExtensionProperty
-    {
-        [JsonExtensionData]
-        public IDictionary<string, JsonElement> MyOverflow { get; set; }
-    }
-
-    public class ClassWithEmptyPropertyNameAndExtensionProperty
-    {
-        [JsonPropertyName("")]
-        public int MyInt1 { get; set; }
-
-        [JsonExtensionData]
-        public IDictionary<string, JsonElement> MyOverflow { get; set; }
     }
 }
index 3485690..a59430f 100644 (file)
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFrameworks>$(NetCoreAppCurrent);net461</TargetFrameworks>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -43,6 +43,7 @@
     <Compile Include="..\Common\JsonSerializerWrapperForStream.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\JsonSerializerWrapperForStream.cs" />
     <Compile Include="..\Common\JsonSerializerWrapperForString.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\JsonSerializerWrapperForString.cs" />
     <Compile Include="..\Common\JsonTestHelper.cs" Link="CommonTest\System\Text\Json\JsonTestHelper.cs" />
+    <Compile Include="..\Common\PropertyNameTests.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\PropertyNameTests.cs" />
     <Compile Include="..\Common\PropertyVisibilityTests.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\PropertyVisibilityTests.cs" />
     <Compile Include="..\Common\PropertyVisibilityTests.InitOnly.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\PropertyVisibilityTests.InitOnly.cs" />
     <Compile Include="..\Common\PropertyVisibilityTests.NonPublicAccessors.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\PropertyVisibilityTests.NonPublicAccessors.cs" />