Skip to content

Commit 1f1ade9

Browse files
committed
Add gRPC JSON transcoding option for case insensitive field names
1 parent 62a224e commit 1f1ade9

File tree

5 files changed

+178
-11
lines changed

5 files changed

+178
-11
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 field 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 field names exactly, including case.
43+
/// Enabling this option may reduce interoperability, as case-insensitive field 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 FieldNamesCaseInsensitive { 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.FieldNamesCaseInsensitive,
5051
};
5152
options.Converters.Add(new NullValueConverter());
5253
options.Converters.Add(new ByteStringConverter());

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Collections;
54
using System.Diagnostics;
65
using System.Diagnostics.CodeAnalysis;
76
using System.Text.Json;

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

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

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

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

505+
[Fact]
506+
public void MapKeyBool_CaseInsensitive()
507+
{
508+
var json = @"{
509+
""mapKeybool"": {
510+
""TRUE"": ""value1"",
511+
""FALSE"": ""value2""
512+
}
513+
}";
514+
515+
AssertReadJson<HelloRequest>(json, serializeOld: false);
516+
}
517+
454518
[Fact]
455519
public void MapKeyInt()
456520
{
@@ -474,6 +538,16 @@ public void OneOf_Success()
474538
AssertReadJson<HelloRequest>(json);
475539
}
476540

541+
[Fact]
542+
public void OneOf_CaseInsensitive_Success()
543+
{
544+
var json = @"{
545+
""ONEOFNAME1"": ""test""
546+
}";
547+
548+
AssertReadJson<HelloRequest>(json, serializeOld: false, settings: new GrpcJsonSettings { FieldNamesCaseInsensitive = true });
549+
}
550+
477551
[Fact]
478552
public void OneOf_Failure()
479553
{
@@ -485,6 +559,17 @@ public void OneOf_Failure()
485559
AssertReadJsonError<HelloRequest>(json, ex => Assert.Equal("Multiple values specified for oneof oneof_test", ex.Message.TrimEnd('.')));
486560
}
487561

562+
[Fact]
563+
public void OneOf_CaseInsensitive_Failure()
564+
{
565+
var json = @"{
566+
""ONEOFNAME1"": ""test"",
567+
""ONEOFNAME2"": ""test""
568+
}";
569+
570+
AssertReadJsonError<HelloRequest>(json, ex => Assert.Equal("Multiple values specified for oneof oneof_test", ex.Message.TrimEnd('.')), deserializeOld: false, settings: new GrpcJsonSettings { FieldNamesCaseInsensitive = true });
571+
}
572+
488573
[Fact]
489574
public void NullableWrappers_NaN()
490575
{
@@ -528,7 +613,27 @@ public void NullableWrappers()
528613
""bytesValue"": ""SGVsbG8gd29ybGQ=""
529614
}";
530615

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

534639
[Fact]
@@ -609,8 +714,19 @@ public void JsonNamePriority_JsonName()
609714
{
610715
var json = @"{""b"":10,""a"":20,""d"":30}";
611716

612-
// TODO: Current Google.Protobuf version doesn't have fix. Update when available. 3.23.0 or later?
613-
var m = AssertReadJson<Issue047349Message>(json, serializeOld: false);
717+
var m = AssertReadJson<Issue047349Message>(json);
718+
719+
Assert.Equal(10, m.A);
720+
Assert.Equal(20, m.B);
721+
Assert.Equal(30, m.C);
722+
}
723+
724+
[Fact]
725+
public void JsonNamePriority_CaseInsensitive_JsonName()
726+
{
727+
var json = @"{""B"":10,""A"":20,""D"":30}";
728+
729+
var m = AssertReadJson<Issue047349Message>(json, serializeOld: false, settings: new GrpcJsonSettings { FieldNamesCaseInsensitive = true });
614730

615731
Assert.Equal(10, m.A);
616732
Assert.Equal(20, m.B);
@@ -622,14 +738,44 @@ public void JsonNamePriority_FieldNameFallback()
622738
{
623739
var json = @"{""b"":10,""a"":20,""c"":30}";
624740

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

628743
Assert.Equal(10, m.A);
629744
Assert.Equal(20, m.B);
630745
Assert.Equal(30, m.C);
631746
}
632747

748+
[Fact]
749+
public void JsonNamePriority_CaseInsensitive_FieldNameFallback()
750+
{
751+
var json = @"{""B"":10,""A"":20,""C"":30}";
752+
753+
var m = AssertReadJson<Issue047349Message>(json, serializeOld: false, settings: new GrpcJsonSettings { FieldNamesCaseInsensitive = true });
754+
755+
Assert.Equal(10, m.A);
756+
Assert.Equal(20, m.B);
757+
Assert.Equal(30, m.C);
758+
}
759+
760+
[Fact]
761+
public void FieldNameCase_Success()
762+
{
763+
var json = @"{""a"":10,""A"":20}";
764+
765+
var m = AssertReadJson<FieldNameCaseMessage>(json);
766+
767+
Assert.Equal(10, m.A);
768+
Assert.Equal(20, m.B);
769+
}
770+
771+
[Fact]
772+
public void FieldNameCase_CaseInsensitive_Failure()
773+
{
774+
var json = @"{""a"":10,""A"":20}";
775+
776+
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 { FieldNamesCaseInsensitive = true });
777+
}
778+
633779
private TValue AssertReadJson<TValue>(string value, GrpcJsonSettings? settings = null, DescriptorRegistry? descriptorRegistry = null, bool serializeOld = true) where TValue : IMessage, new()
634780
{
635781
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)