diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/GrpcJsonSettings.cs b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/GrpcJsonSettings.cs index d53ca301dc01..b18519c76e0a 100644 --- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/GrpcJsonSettings.cs +++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/GrpcJsonSettings.cs @@ -11,25 +11,41 @@ public sealed class GrpcJsonSettings /// /// Gets or sets a value that indicates whether fields with default values are ignored during serialization. /// This setting only affects fields which don't support "presence", such as singular non-optional proto3 primitive fields. - /// Default value is false. + /// Default value is . /// public bool IgnoreDefaultValues { get; set; } /// /// Gets or sets a value that indicates whether values are written as integers instead of strings. - /// Default value is false. + /// Default value is . /// public bool WriteEnumsAsIntegers { get; set; } /// /// Gets or sets a value that indicates whether and values are written as strings instead of numbers. - /// Default value is false. + /// Default value is . /// public bool WriteInt64sAsStrings { get; set; } /// /// Gets or sets a value that indicates whether JSON should use pretty printing. - /// Default value is false. + /// Default value is . /// public bool WriteIndented { get; set; } + + /// + /// Gets or sets a value that indicates whether property names are compared using case-insensitive matching during deserialization. + /// The default value is . + /// + /// + /// + /// The Protobuf JSON specification requires JSON property names to match message field names exactly, including case. + /// Enabling this option may reduce interoperability, as case-insensitive property matching might not be supported + /// by other JSON transcoding implementations. + /// + /// + /// For more information, see . + /// + /// + public bool PropertyNameCaseInsensitive { get; set; } } diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/JsonConverterHelper.cs b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/JsonConverterHelper.cs index 0f5ebefdce18..c207730bc87b 100644 --- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/JsonConverterHelper.cs +++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/JsonConverterHelper.cs @@ -46,7 +46,8 @@ internal static JsonSerializerOptions CreateSerializerOptions(JsonContext contex WriteIndented = writeIndented, NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - TypeInfoResolver = typeInfoResolver + TypeInfoResolver = typeInfoResolver, + PropertyNameCaseInsensitive = context.Settings.PropertyNameCaseInsensitive, }; options.Converters.Add(new NullValueConverter()); options.Converters.Add(new ByteStringConverter()); diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/MessageTypeInfoResolver.cs b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/MessageTypeInfoResolver.cs index 66e249194e0c..d41024ec89fa 100644 --- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/MessageTypeInfoResolver.cs +++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/MessageTypeInfoResolver.cs @@ -1,7 +1,6 @@ // 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; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json; diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/PublicAPI.Unshipped.txt b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/PublicAPI.Unshipped.txt index 7dc5c58110bf..6da56881ef04 100644 --- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/PublicAPI.Unshipped.txt +++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.PropertyNameCaseInsensitive.get -> bool +Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.PropertyNameCaseInsensitive.set -> void diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/ConverterTests/JsonConverterReadTests.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/ConverterTests/JsonConverterReadTests.cs index 519f40221216..52e056662a9c 100644 --- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/ConverterTests/JsonConverterReadTests.cs +++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/ConverterTests/JsonConverterReadTests.cs @@ -59,6 +59,40 @@ public void JsonCustomizedName() Assert.Equal("A field name", m.FieldName); } + [Fact] + public void NonJsonName_CaseInsensitive() + { + var json = @"{ + ""HIDING_FIELD_NAME"": ""A field name"" +}"; + + var m = AssertReadJson(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true }); + Assert.Equal("A field name", m.HidingFieldName); + } + + [Fact] + public void HidingJsonName_CaseInsensitive() + { + var json = @"{ + ""FIELD_NAME"": ""A field name"" +}"; + + var m = AssertReadJson(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true }); + Assert.Equal("", m.FieldName); + Assert.Equal("A field name", m.HidingFieldName); + } + + [Fact] + public void JsonCustomizedName_CaseInsensitive() + { + var json = @"{ + ""JSON_CUSTOMIZED_NAME"": ""A field name"" +}"; + + var m = AssertReadJson(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true }); + Assert.Equal("A field name", m.FieldName); + } + [Fact] public void ReadObjectProperties() { @@ -438,6 +472,23 @@ public void MapMessages() AssertReadJson(json); } + [Fact] + public void MapMessages_CaseInsensitive() + { + var json = @"{ + ""mapMessage"": { + ""name1"": { + ""SUBFIELD"": ""value1"" + }, + ""name2"": { + ""SUBFIELD"": ""value2"" + } + } +}"; + + AssertReadJson(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true }); + } + [Fact] public void MapKeyBool() { @@ -451,6 +502,21 @@ public void MapKeyBool() AssertReadJson(json); } + [Fact] + public void MapKeyBool_CaseInsensitive() + { + var json = @"{ + ""mapKeybool"": { + ""TRUE"": ""value1"", + ""FALSE"": ""value2"" + } +}"; + + // Note: JSON property names here are keys in a dictionary, not fields. So FieldNamesCaseInsensitive doesn't apply. + // The new serializer supports converting true/false to boolean keys while ignoring case. + AssertReadJson(json, serializeOld: false); + } + [Fact] public void MapKeyInt() { @@ -474,6 +540,16 @@ public void OneOf_Success() AssertReadJson(json); } + [Fact] + public void OneOf_CaseInsensitive_Success() + { + var json = @"{ + ""ONEOFNAME1"": ""test"" +}"; + + AssertReadJson(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true }); + } + [Fact] public void OneOf_Failure() { @@ -485,6 +561,17 @@ public void OneOf_Failure() AssertReadJsonError(json, ex => Assert.Equal("Multiple values specified for oneof oneof_test", ex.Message.TrimEnd('.'))); } + [Fact] + public void OneOf_CaseInsensitive_Failure() + { + var json = @"{ + ""ONEOFNAME1"": ""test"", + ""ONEOFNAME2"": ""test"" +}"; + + AssertReadJsonError(json, ex => Assert.Equal("Multiple values specified for oneof oneof_test", ex.Message.TrimEnd('.')), deserializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true }); + } + [Fact] public void NullableWrappers_NaN() { @@ -528,7 +615,27 @@ public void NullableWrappers() ""bytesValue"": ""SGVsbG8gd29ybGQ="" }"; - AssertReadJson(json); + var result = AssertReadJson(json); + Assert.Equal("A string", result.StringValue); + } + + [Fact] + public void NullableWrappers_CaseInsensitive() + { + var json = @"{ + ""STRINGVALUE"": ""A string"", + ""INT32VALUE"": 1, + ""INT64VALUE"": ""2"", + ""FLOATVALUE"": 1.2, + ""DOUBLEVALUE"": 1.1, + ""BOOLVALUE"": true, + ""UINT32VALUE"": 3, + ""UINT64VALUE"": ""4"", + ""BYTESVALUE"": ""SGVsbG8gd29ybGQ="" +}"; + + var result = AssertReadJson(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true }); + Assert.Equal("A string", result.StringValue); } [Fact] @@ -609,8 +716,19 @@ public void JsonNamePriority_JsonName() { var json = @"{""b"":10,""a"":20,""d"":30}"; - // TODO: Current Google.Protobuf version doesn't have fix. Update when available. 3.23.0 or later? - var m = AssertReadJson(json, serializeOld: false); + var m = AssertReadJson(json); + + Assert.Equal(10, m.A); + Assert.Equal(20, m.B); + Assert.Equal(30, m.C); + } + + [Fact] + public void JsonNamePriority_CaseInsensitive_JsonName() + { + var json = @"{""B"":10,""A"":20,""D"":30}"; + + var m = AssertReadJson(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true }); Assert.Equal(10, m.A); Assert.Equal(20, m.B); @@ -622,14 +740,44 @@ public void JsonNamePriority_FieldNameFallback() { var json = @"{""b"":10,""a"":20,""c"":30}"; - // TODO: Current Google.Protobuf version doesn't have fix. Update when available. 3.23.0 or later? - var m = AssertReadJson(json, serializeOld: false); + var m = AssertReadJson(json); Assert.Equal(10, m.A); Assert.Equal(20, m.B); Assert.Equal(30, m.C); } + [Fact] + public void JsonNamePriority_CaseInsensitive_FieldNameFallback() + { + var json = @"{""B"":10,""A"":20,""C"":30}"; + + var m = AssertReadJson(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true }); + + Assert.Equal(10, m.A); + Assert.Equal(20, m.B); + Assert.Equal(30, m.C); + } + + [Fact] + public void FieldNameCase_Success() + { + var json = @"{""a"":10,""A"":20}"; + + var m = AssertReadJson(json); + + Assert.Equal(10, m.A); + Assert.Equal(20, m.B); + } + + [Fact] + public void FieldNameCase_CaseInsensitive_Failure() + { + var json = @"{""a"":10,""A"":20}"; + + AssertReadJsonError(json, ex => Assert.Equal("The JSON property name for 'Transcoding.FieldNameCaseMessage.A' collides with another property.", ex.Message), deserializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true }); + } + private TValue AssertReadJson(string value, GrpcJsonSettings? settings = null, DescriptorRegistry? descriptorRegistry = null, bool serializeOld = true) where TValue : IMessage, new() { var typeRegistery = TypeRegistry.FromFiles( diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto index e44535586bba..152e434607da 100644 --- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto +++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto @@ -235,3 +235,8 @@ message HelloReply { message NullValueContainer { google.protobuf.NullValue null_value = 1; } + +message FieldNameCaseMessage { + int32 a = 1; + int32 b = 2 [json_name="A"]; +}