From: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Aug 2021 15:46:30 +0000 (-0700) Subject: [release/6.0-rc1] Support special characters in [JsonPropertyName] (#58075) X-Git-Tag: accepted/tizen/unified/20220110.054933~222^2~20 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=614525189115a83e84e23041f8d5f7bc4cc5cf09;p=platform%2Fupstream%2Fdotnet%2Fruntime.git [release/6.0-rc1] Support special characters in [JsonPropertyName] (#58075) * Support special characters in [JsonPropertyName] * Make gen'd property name readonly; other misc Co-authored-by: Steve Harter --- diff --git a/src/libraries/System.Text.Json/gen/ContextGenerationSpec.cs b/src/libraries/System.Text.Json/gen/ContextGenerationSpec.cs index 545d562..13e9c74 100644 --- a/src/libraries/System.Text.Json/gen/ContextGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/ContextGenerationSpec.cs @@ -32,9 +32,11 @@ namespace System.Text.Json.SourceGeneration public HashSet TypesWithMetadataGenerated { get; } = new(); /// - /// 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]. /// - public HashSet RuntimePropertyNames { get; } = new(); + public Dictionary RuntimePropertyNames { get; } = new(); public string ContextTypeRef => ContextType.GetCompilableName(); } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 75d8cdd..fafe991 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -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 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(); diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 33a6056..b75b7a6 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -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); diff --git a/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs b/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs index 9d05fdf..74740cc 100644 --- a/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs @@ -32,6 +32,8 @@ namespace System.Text.Json.SourceGeneration /// public string RuntimePropertyName { get; init; } + public string PropertyNameVarName { get; init; } + /// /// Whether the property has a set method. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs index b539d6d..4c4b4e0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs @@ -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 index 0000000..fe1c4b5 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs @@ -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(@"{""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(@"{""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(@"{}", 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(@"{""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(async () => await JsonSerializerWrapperForString.DeserializeWrapper(@"{}", options)); + await Assert.ThrowsAsync(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(@"{""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(@"{""myint16"":1}", options); + Assert.Equal(0, obj.MyInt16); + } + + { + var options = new JsonSerializerOptions(); + options.PropertyNameCaseInsensitive = true; + SimpleTestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper(@"{""myint16"":1}", options); + Assert.Equal(1, obj.MyInt16); + } + } + + [Fact] + public async Task JsonPropertyNameAttribute() + { + { + OverridePropertyNameDesignTime_TestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper(@"{""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(@"{""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(async () => await JsonSerializerWrapperForString.DeserializeWrapper("{}", options)); + } + + { + var options = new JsonSerializerOptions(); + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(new DuplicatePropertyNameDesignTime_TestClass(), options)); + } + } + + [Fact] + public async Task JsonNameConflictOnCamelCasingFail() + { + { + // Baseline comparison - no options set. + IntPropertyNamesDifferentByCaseOnly_TestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper("{}"); + await JsonSerializerWrapperForString.SerializeWrapper(obj); + } + + { + var options = new JsonSerializerOptions(); + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper("{}", options)); + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(new IntPropertyNamesDifferentByCaseOnly_TestClass(), options)); + } + + { + // Baseline comparison - no options set. + ObjectPropertyNamesDifferentByCaseOnly_TestClass obj = await JsonSerializerWrapperForString.DeserializeWrapper("{}"); + await JsonSerializerWrapperForString.SerializeWrapper(obj); + } + + { + var options = new JsonSerializerOptions(); + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper("{}", options)); + await Assert.ThrowsAsync(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(json); + Assert.Equal(1, obj.MyInt1); + } + } + + [Fact] + public async Task UnicodePropertyNames() + { + ClassWithUnicodeProperty obj = await JsonSerializerWrapperForString.DeserializeWrapper("{\"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("{\"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(json); + Assert.Equal(1, obj.A\u0467); + + // With custom escaper + obj = await JsonSerializerWrapperForString.DeserializeWrapper(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("{\"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(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(json); + + // Verify round-tripped object. + Verify(); + } + + [Fact] + public async Task BadNamingPolicy_ThrowsInvalidOperation() + { + var options = new JsonSerializerOptions { DictionaryKeyPolicy = new NullNamingPolicy() }; + + var inputPrimitive = new Dictionary + { + { "validKey", 1 } + }; + + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(inputPrimitive, options)); + + var inputClass = new Dictionary + { + { "validKey", new OverridePropertyNameDesignTime_TestClass() } + }; + + await Assert.ThrowsAsync(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(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 index 0000000..1beb220 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyNameTests.cs @@ -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))] + [JsonSerializable(typeof(Dictionary))] + [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))] + [JsonSerializable(typeof(Dictionary))] + [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 + { + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj index 3fe38b9..e848e3e 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj @@ -41,6 +41,7 @@ + @@ -72,6 +73,7 @@ + diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ExtensionDataTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ExtensionDataTests.cs index 853ec36..7a64f8e 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ExtensionDataTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ExtensionDataTests.cs @@ -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(json, options); + Assert.Equal(1, obj.MyInt1); + Assert.Null(obj.MyOverflow); + } + + [Fact] + public static void EmptyPropertyNameInExtensionData() + { + { + string json = @"{"""":42}"; + EmptyClassWithExtensionProperty obj = JsonSerializer.Deserialize(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(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(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(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(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(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(@"{""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(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 MyOverflow { get; set; } + } + + public class ClassWithEmptyPropertyNameAndExtensionProperty + { + [JsonPropertyName("")] + public int MyInt1 { get; set; } + + [JsonExtensionData] + public IDictionary MyOverflow { get; set; } + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyNameTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyNameTests.cs index 6a14f3e..1423174 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyNameTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyNameTests.cs @@ -1,185 +1,28 @@ -// 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(@"{""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(@"{""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(@"{}", 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(@"{""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(() => JsonSerializer.Deserialize(@"{}", options)); - Assert.Throws(() => 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(@"{""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(@"{""myint16"":1}", options); - Assert.Equal(0, obj.MyInt16); - } - - { - var options = new JsonSerializerOptions(); - options.PropertyNameCaseInsensitive = true; - SimpleTestClass obj = JsonSerializer.Deserialize(@"{""myint16"":1}", options); - Assert.Equal(1, obj.MyInt16); - } - } - - [Fact] - public static void JsonPropertyNameAttribute() - { - { - OverridePropertyNameDesignTime_TestClass obj = JsonSerializer.Deserialize(@"{""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(@"{""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(() => JsonSerializer.Deserialize("{}", options)); - } - - { - var options = new JsonSerializerOptions(); - Assert.Throws(() => 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(() => JsonSerializer.Serialize(new NullPropertyName_TestClass(), options)); + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(new NullPropertyName_TestClass(), options)); } [Fact] - public static void JsonNameConflictOnCamelCasingFail() - { - { - // Baseline comparison - no options set. - IntPropertyNamesDifferentByCaseOnly_TestClass obj = JsonSerializer.Deserialize("{}"); - JsonSerializer.Serialize(obj); - } - - { - var options = new JsonSerializerOptions(); - options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - - Assert.Throws(() => JsonSerializer.Deserialize("{}", options)); - Assert.Throws(() => JsonSerializer.Serialize(new IntPropertyNamesDifferentByCaseOnly_TestClass(), options)); - } - - { - // Baseline comparison - no options set. - ObjectPropertyNamesDifferentByCaseOnly_TestClass obj = JsonSerializer.Deserialize("{}"); - JsonSerializer.Serialize(obj); - } - - { - var options = new JsonSerializerOptions(); - options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - - Assert.Throws(() => JsonSerializer.Deserialize("{}", options)); - Assert.Throws(() => 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(() => JsonSerializer.Deserialize(json, options)); - Assert.Throws(() => 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(json); - Assert.Equal(1, obj.MyInt1); - } - } - - [Fact] - public static void EmptyPropertyNameInExtensionData() - { - { - string json = @"{"""":42}"; - EmptyClassWithExtensionProperty obj = JsonSerializer.Deserialize(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(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(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(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(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(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(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("{\"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("{\"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(json); - Assert.Equal(1, obj.A\u0467); - - // With custom escaper - obj = JsonSerializer.Deserialize(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("{\"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(json); - Assert.Equal(1, obj.A\u046734567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890); - } - - [Fact] - public static void ExtensionDataDictionarySerialize_DoesNotHonor() - { - var options = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - - EmptyClassWithExtensionProperty obj = JsonSerializer.Deserialize(@"{""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(async () => await JsonSerializerWrapperForString.DeserializeWrapper(json, options)); + await Assert.ThrowsAsync(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(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(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 - { - { "validKey", 1 } - }; - - Assert.Throws(() => JsonSerializer.Serialize(inputPrimitive, options)); - - var inputClass = new Dictionary - { - { "validKey", new OverridePropertyNameDesignTime_TestClass() } - }; - - Assert.Throws(() => 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 MyOverflow { get; set; } - } - - public class ClassWithEmptyPropertyNameAndExtensionProperty - { - [JsonPropertyName("")] - public int MyInt1 { get; set; } - - [JsonExtensionData] - public IDictionary MyOverflow { get; set; } } } 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 3485690..a59430f 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 @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);net461 true @@ -43,6 +43,7 @@ +