Repository: dotnet/aspnetcore (2025-07-22T23:26:21Z)

Issue:
Make PropertyNameCaseInsensitive available for JSON->gRPC transcoding
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Is your feature request related to a problem? Please describe the problem.

I am trying to make a drop-in replacement microservice that supports both REST and gRPC for an existing microservice that is REST-only, but my issue is that the current microservice is case insensitive, but grpc/json transcoding cannot be made to be.


### Describe the solution you'd like

Add PropertyNameCaseInsensitive to GrpcJsonSettings 
Initialize JsonSerializerOptions with this flag in JsonConverterHelper.CreateSerializerOptions()

### Additional context

_No response_


Discussion:
I got the same issue and here's a temporary workaround:
```
        grpcServerBuilder.AddJsonTranscoding((o) =>
        {
            var prop = o.GetType().GetProperty("UnarySerializerOptions", System.Reflection.BindingFlags.NonPublic
                                                                         | System.Reflection.BindingFlags.Instance);
            JsonSerializerOptions option= (JsonSerializerOptions) prop.GetValue(o);
            option!.PropertyNameCaseInsensitive = true;
                      
            prop = o.GetType().GetProperty("ServerStreamingSerializerOptions", System.Reflection.BindingFlags.NonPublic
                                                                         | System.Reflection.BindingFlags.Instance);
            option= (JsonSerializerOptions) prop.GetValue(o);
            option!.PropertyNameCaseInsensitive = true;
            
        });
```

I hope it could be one of the option for GrpcJsonTranscodingOptions.
> I got the same issue and here's a temporary workaround:
> 
> ```
>         grpcServerBuilder.AddJsonTranscoding((o) =>
>         {
>             var prop = o.GetType().GetProperty("UnarySerializerOptions", System.Reflection.BindingFlags.NonPublic
>                                                                          | System.Reflection.BindingFlags.Instance);
>             JsonSerializerOptions option= (JsonSerializerOptions) prop.GetValue(o);
>             option!.PropertyNameCaseInsensitive = true;
>                       
>             prop = o.GetType().GetProperty("ServerStreamingSerializerOptions", System.Reflection.BindingFlags.NonPublic
>                                                                          | System.Reflection.BindingFlags.Instance);
>             option= (JsonSerializerOptions) prop.GetValue(o);
>             option!.PropertyNameCaseInsensitive = true;
>             
>         });
> ```
> 
> I hope it could be one of the option for GrpcJsonTranscodingOptions.

It seems not working in .NET 8. I want to use lowerCamelCase style input/output but no way to do it.
Here is another workaround for .NET 8

```csharp
builder.Services.AddGrpc().AddJsonTranscoding();
builder.Services.PostConfigure<GrpcJsonTranscodingOptions>(options =>
{
    foreach (var name in new[] { "UnarySerializerOptions", "ServerStreamingSerializerOptions" })
    {
        var prop = options.GetType().GetProperty(name,
            System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        var serializerOptions = prop!.GetValue(options) as JsonSerializerOptions;
        serializerOptions!.PropertyNameCaseInsensitive = true;
    }
});
```
When i try to use AddJsonTranscoding(), i get this error message:

Exception was thrown by handler. InvalidCastException: Unable to cast object of type 'Google.Protobuf.WellKnownTypes.StringValue' to type 'System.String'.

How can i can solve this issue? Thank you.



Patch:
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
     /// <summary>
     /// 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 <see langword="false"/>.
     /// </summary>
     public bool IgnoreDefaultValues { get; set; }
 
     /// <summary>
     /// Gets or sets a value that indicates whether <see cref="Enum"/> values are written as integers instead of strings.
-    /// Default value is false.
+    /// Default value is <see langword="false"/>.
     /// </summary>
     public bool WriteEnumsAsIntegers { get; set; }
 
     /// <summary>
     /// Gets or sets a value that indicates whether <see cref="long"/> and <see cref="ulong"/> values are written as strings instead of numbers.
-    /// Default value is false.
+    /// Default value is <see langword="false"/>.
     /// </summary>
     public bool WriteInt64sAsStrings { get; set; }
 
     /// <summary>
     /// Gets or sets a value that indicates whether JSON should use pretty printing.
-    /// Default value is false.
+    /// Default value is <see langword="false"/>.
     /// </summary>
     public bool WriteIndented { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value that indicates whether property names are compared using case-insensitive matching during deserialization.
+    /// The default value is <see langword="false"/>.
+    /// </summary>
+    /// <remarks>
+    /// <para>
+    /// 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.
+    /// </para>
+    /// <para>
+    /// For more information, see <see href="https://protobuf.dev/programming-guides/json/"/>.
+    /// </para>
+    /// </remarks>
+    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 +1,3 @@
 #nullable enable
+Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.PropertyNameCaseInsensitive.get -> bool
+Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.PropertyNameCaseInsensitive.set -> void


Test patch:
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<HelloRequest>(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<HelloRequest>(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<HelloRequest>(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<HelloRequest>(json);
     }
 
+    [Fact]
+    public void MapMessages_CaseInsensitive()
+    {
+        var json = @"{
+  ""mapMessage"": {
+    ""name1"": {
+      ""SUBFIELD"": ""value1""
+    },
+    ""name2"": {
+      ""SUBFIELD"": ""value2""
+    }
+  }
+}";
+
+        AssertReadJson<HelloRequest>(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true });
+    }
+
     [Fact]
     public void MapKeyBool()
     {
@@ -451,6 +502,21 @@ public void MapKeyBool()
         AssertReadJson<HelloRequest>(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<HelloRequest>(json, serializeOld: false);
+    }
+
     [Fact]
     public void MapKeyInt()
     {
@@ -474,6 +540,16 @@ public void OneOf_Success()
         AssertReadJson<HelloRequest>(json);
     }
 
+    [Fact]
+    public void OneOf_CaseInsensitive_Success()
+    {
+        var json = @"{
+  ""ONEOFNAME1"": ""test""
+}";
+
+        AssertReadJson<HelloRequest>(json, serializeOld: false, settings: new GrpcJsonSettings { PropertyNameCaseInsensitive = true });
+    }
+
     [Fact]
     public void OneOf_Failure()
     {
@@ -485,6 +561,17 @@ public void OneOf_Failure()
         AssertReadJsonError<HelloRequest>(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<HelloRequest>(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<HelloRequest.Types.Wrappers>(json);
+        var result = AssertReadJson<HelloRequest.Types.Wrappers>(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<HelloRequest.Types.Wrappers>(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<Issue047349Message>(json, serializeOld: false);
+        var m = AssertReadJson<Issue047349Message>(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<Issue047349Message>(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<Issue047349Message>(json, serializeOld: false);
+        var m = AssertReadJson<Issue047349Message>(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<Issue047349Message>(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<FieldNameCaseMessage>(json);
+
+        Assert.Equal(10, m.A);
+        Assert.Equal(20, m.B);
+    }
+
+    [Fact]
+    public void FieldNameCase_CaseInsensitive_Failure()
+    {
+        var json = @"{""a"":10,""A"":20}";
+
+        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 });
+    }
+
     private TValue AssertReadJson<TValue>(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"];
+}