Skip to content

Commit f4748e3

Browse files
authored
Add gRPC JSON transcoding option for case insensitive field names (#62868)
1 parent c40d32e commit f4748e3

File tree

5 files changed

+182
-10
lines changed

5 files changed

+182
-10
lines changed

src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/GrpcJsonSettings.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,41 @@ public sealed class GrpcJsonSettings
1111
/// <summary>
1212
/// Gets or sets a value that indicates whether fields with default values are ignored during serialization.
1313
/// This setting only affects fields which don't support "presence", such as singular non-optional proto3 primitive fields.
14-
/// Default value is false.
14+
/// Default value is <see langword="false"/>.
1515
/// </summary>
1616
public bool IgnoreDefaultValues { get; set; }
1717

1818
/// <summary>
1919
/// Gets or sets a value that indicates whether <see cref="Enum"/> values are written as integers instead of strings.
20-
/// Default value is false.
20+
/// Default value is <see langword="false"/>.
2121
/// </summary>
2222
public bool WriteEnumsAsIntegers { get; set; }
2323

2424
/// <summary>
2525
/// Gets or sets a value that indicates whether <see cref="long"/> and <see cref="ulong"/> values are written as strings instead of numbers.
26-
/// Default value is false.
26+
/// Default value is <see langword="false"/>.
2727
/// </summary>
2828
public bool WriteInt64sAsStrings { get; set; }
2929

3030
/// <summary>
3131
/// Gets or sets a value that indicates whether JSON should use pretty printing.
32-
/// Default value is false.
32+
/// Default value is <see langword="false"/>.
3333
/// </summary>
3434
public bool WriteIndented { get; set; }
35+
36+
/// <summary>
37+
/// Gets or sets a value that indicates whether property names are compared using case-insensitive matching during deserialization.
38+
/// The default value is <see langword="false"/>.
39+
/// </summary>
40+
/// <remarks>
41+
/// <para>
42+
/// The Protobuf JSON specification requires JSON property names to match message field names exactly, including case.
43+
/// Enabling this option may reduce interoperability, as case-insensitive property matching might not be supported
44+
/// by other JSON transcoding implementations.
45+
/// </para>
46+
/// <para>
47+
/// For more information, see <see href="https://protobuf.dev/programming-guides/json/"/>.
48+
/// </para>
49+
/// </remarks>
50+
public bool PropertyNameCaseInsensitive { get; set; }
3551
}

src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/JsonConverterHelper.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ internal static JsonSerializerOptions CreateSerializerOptions(JsonContext contex
4646
WriteIndented = writeIndented,
4747
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
4848
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
49-
TypeInfoResolver = typeInfoResolver
49+
TypeInfoResolver = typeInfoResolver,
50+
PropertyNameCaseInsensitive = context.Settings.PropertyNameCaseInsensitive,
5051
};
5152
options.Converters.Add(new NullValueConverter());
5253
options.Converters.Add(new ByteStringConverter());
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.PropertyNameCaseInsensitive.get -> bool
3+
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.PropertyNameCaseInsensitive.set -> void

src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/ConverterTests/JsonConverterReadTests.cs

Lines changed: 153 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,40 @@ public void JsonCustomizedName()
6060
Assert.Equal("A field name", m.FieldName);
6161
}
6262

63+
[Fact]
64+
public void NonJsonName_CaseInsensitive()
65+
{
66+
var json = @"{
67+
""HIDING_FIELD_NAME"": ""A field name""
68+
}";
69+
70+
var m = AssertReadJson<HelloRequest>(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true });
71+
Assert.Equal("A field name", m.HidingFieldName);
72+
}
73+
74+
[Fact]
75+
public void HidingJsonName_CaseInsensitive()
76+
{
77+
var json = @"{
78+
""FIELD_NAME"": ""A field name""
79+
}";
80+
81+
var m = AssertReadJson<HelloRequest>(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true });
82+
Assert.Equal("", m.FieldName);
83+
Assert.Equal("A field name", m.HidingFieldName);
84+
}
85+
86+
[Fact]
87+
public void JsonCustomizedName_CaseInsensitive()
88+
{
89+
var json = @"{
90+
""JSON_CUSTOMIZED_NAME"": ""A field name""
91+
}";
92+
93+
var m = AssertReadJson<HelloRequest>(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true });
94+
Assert.Equal("A field name", m.FieldName);
95+
}
96+
6397
[Fact]
6498
public void ReadObjectProperties()
6599
{
@@ -439,6 +473,23 @@ public void MapMessages()
439473
AssertReadJson<HelloRequest>(json);
440474
}
441475

476+
[Fact]
477+
public void MapMessages_CaseInsensitive()
478+
{
479+
var json = @"{
480+
""mapMessage"": {
481+
""name1"": {
482+
""SUBFIELD"": ""value1""
483+
},
484+
""name2"": {
485+
""SUBFIELD"": ""value2""
486+
}
487+
}
488+
}";
489+
490+
AssertReadJson<HelloRequest>(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true });
491+
}
492+
442493
[Fact]
443494
public void MapKeyBool()
444495
{
@@ -452,6 +503,21 @@ public void MapKeyBool()
452503
AssertReadJson<HelloRequest>(json);
453504
}
454505

506+
[Fact]
507+
public void MapKeyBool_CaseInsensitive()
508+
{
509+
var json = @"{
510+
""mapKeybool"": {
511+
""TRUE"": ""value1"",
512+
""FALSE"": ""value2""
513+
}
514+
}";
515+
516+
// Note: JSON property names here are keys in a dictionary, not fields. So FieldNamesCaseInsensitive doesn't apply.
517+
// The new serializer supports converting true/false to boolean keys while ignoring case.
518+
AssertReadJson<HelloRequest>(json, serializeOld: false);
519+
}
520+
455521
[Fact]
456522
public void MapKeyInt()
457523
{
@@ -475,6 +541,16 @@ public void OneOf_Success()
475541
AssertReadJson<HelloRequest>(json);
476542
}
477543

544+
[Fact]
545+
public void OneOf_CaseInsensitive_Success()
546+
{
547+
var json = @"{
548+
""ONEOFNAME1"": ""test""
549+
}";
550+
551+
AssertReadJson<HelloRequest>(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true });
552+
}
553+
478554
[Fact]
479555
public void OneOf_Failure()
480556
{
@@ -486,6 +562,17 @@ public void OneOf_Failure()
486562
AssertReadJsonError<HelloRequest>(json, ex => Assert.Equal("Multiple values specified for oneof oneof_test", ex.Message.TrimEnd('.')));
487563
}
488564

565+
[Fact]
566+
public void OneOf_CaseInsensitive_Failure()
567+
{
568+
var json = @"{
569+
""ONEOFNAME1"": ""test"",
570+
""ONEOFNAME2"": ""test""
571+
}";
572+
573+
AssertReadJsonError<HelloRequest>(json, ex => Assert.Equal("Multiple values specified for oneof oneof_test", ex.Message.TrimEnd('.')), deserializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true });
574+
}
575+
489576
[Fact]
490577
public void NullableWrappers_NaN()
491578
{
@@ -529,7 +616,27 @@ public void NullableWrappers()
529616
""bytesValue"": ""SGVsbG8gd29ybGQ=""
530617
}";
531618

532-
AssertReadJson<HelloRequest.Types.Wrappers>(json);
619+
var result = AssertReadJson<HelloRequest.Types.Wrappers>(json);
620+
Assert.Equal("A string", result.StringValue);
621+
}
622+
623+
[Fact]
624+
public void NullableWrappers_CaseInsensitive()
625+
{
626+
var json = @"{
627+
""STRINGVALUE"": ""A string"",
628+
""INT32VALUE"": 1,
629+
""INT64VALUE"": ""2"",
630+
""FLOATVALUE"": 1.2,
631+
""DOUBLEVALUE"": 1.1,
632+
""BOOLVALUE"": true,
633+
""UINT32VALUE"": 3,
634+
""UINT64VALUE"": ""4"",
635+
""BYTESVALUE"": ""SGVsbG8gd29ybGQ=""
636+
}";
637+
638+
var result = AssertReadJson<HelloRequest.Types.Wrappers>(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true });
639+
Assert.Equal("A string", result.StringValue);
533640
}
534641

535642
[Fact]
@@ -629,8 +736,19 @@ public void JsonNamePriority_JsonName()
629736
{
630737
var json = @"{""b"":10,""a"":20,""d"":30}";
631738

632-
// TODO: Current Google.Protobuf version doesn't have fix. Update when available. 3.23.0 or later?
633-
var m = AssertReadJson<Issue047349Message>(json, serializeOld: false);
739+
var m = AssertReadJson<Issue047349Message>(json);
740+
741+
Assert.Equal(10, m.A);
742+
Assert.Equal(20, m.B);
743+
Assert.Equal(30, m.C);
744+
}
745+
746+
[Fact]
747+
public void JsonNamePriority_CaseInsensitive_JsonName()
748+
{
749+
var json = @"{""B"":10,""A"":20,""D"":30}";
750+
751+
var m = AssertReadJson<Issue047349Message>(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true });
634752

635753
Assert.Equal(10, m.A);
636754
Assert.Equal(20, m.B);
@@ -642,14 +760,44 @@ public void JsonNamePriority_FieldNameFallback()
642760
{
643761
var json = @"{""b"":10,""a"":20,""c"":30}";
644762

645-
// TODO: Current Google.Protobuf version doesn't have fix. Update when available. 3.23.0 or later?
646-
var m = AssertReadJson<Issue047349Message>(json, serializeOld: false);
763+
var m = AssertReadJson<Issue047349Message>(json);
647764

648765
Assert.Equal(10, m.A);
649766
Assert.Equal(20, m.B);
650767
Assert.Equal(30, m.C);
651768
}
652769

770+
[Fact]
771+
public void JsonNamePriority_CaseInsensitive_FieldNameFallback()
772+
{
773+
var json = @"{""B"":10,""A"":20,""C"":30}";
774+
775+
var m = AssertReadJson<Issue047349Message>(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true });
776+
777+
Assert.Equal(10, m.A);
778+
Assert.Equal(20, m.B);
779+
Assert.Equal(30, m.C);
780+
}
781+
782+
[Fact]
783+
public void FieldNameCase_Success()
784+
{
785+
var json = @"{""a"":10,""A"":20}";
786+
787+
var m = AssertReadJson<FieldNameCaseMessage>(json);
788+
789+
Assert.Equal(10, m.A);
790+
Assert.Equal(20, m.B);
791+
}
792+
793+
[Fact]
794+
public void FieldNameCase_CaseInsensitive_Failure()
795+
{
796+
var json = @"{""a"":10,""A"":20}";
797+
798+
AssertReadJsonError<FieldNameCaseMessage>(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 });
799+
}
800+
653801
private TValue AssertReadJson<TValue>(string value, GrpcJsonSettings? settings = null, DescriptorRegistry? descriptorRegistry = null, bool serializeOld = true) where TValue : IMessage, new()
654802
{
655803
var typeRegistery = TypeRegistry.FromFiles(

src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,8 @@ message HelloReply {
235235
message NullValueContainer {
236236
google.protobuf.NullValue null_value = 1;
237237
}
238+
239+
message FieldNameCaseMessage {
240+
int32 a = 1;
241+
int32 b = 2 [json_name="A"];
242+
}

0 commit comments

Comments
 (0)