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"];
+}