Skip to content

Don't error on Protobuf messages that expose wrapper types #62871

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -94,6 +93,23 @@ private JsonPropertyInfo CreatePropertyInfo(JsonTypeInfo typeInfo, string name,
JsonConverterHelper.GetFieldType(field),
name);

// A property with a wrapper type is usually the underlying type on the DTO.
// For example, a field of type google.protobuf.StringValue will have a property of type string.
// However, the wrapper type is exposed if someone manually creates a DTO with it, or there is a problem
// detecting wrapper type in code generation. For example, https://github.com/protocolbuffers/protobuf/issues/22744
FieldDescriptor? wrapperTypeValueField = null;
if (field.FieldType == FieldType.Message && ServiceDescriptorHelpers.IsWrapperType(field.MessageType))
{
var property = field.ContainingType.ClrType.GetProperty(field.PropertyName);

// Check if the property type is the same as the field type. This means the property is StringValue, et al,
// and additional conversion is required.
if (property != null && property.PropertyType == field.MessageType.ClrType)
{
wrapperTypeValueField = field.MessageType.FindFieldByName("value");
}
}

propertyInfo.ShouldSerialize = (o, v) =>
{
// Properties that don't have this flag set are only used to deserialize incoming JSON.
Expand All @@ -105,7 +121,13 @@ private JsonPropertyInfo CreatePropertyInfo(JsonTypeInfo typeInfo, string name,
};
propertyInfo.Get = (o) =>
{
return field.Accessor.GetValue((IMessage)o);
var value = field.Accessor.GetValue((IMessage)o);
if (wrapperTypeValueField != null && value is IMessage wrapperMessage)
{
return wrapperTypeValueField.Accessor.GetValue(wrapperMessage);
}

return value;
};

if (field.IsMap || field.IsRepeated)
Expand All @@ -115,13 +137,13 @@ private JsonPropertyInfo CreatePropertyInfo(JsonTypeInfo typeInfo, string name,
}
else
{
propertyInfo.Set = GetSetMethod(field);
propertyInfo.Set = GetSetMethod(field, wrapperTypeValueField);
}

return propertyInfo;
}

private static Action<object, object?> GetSetMethod(FieldDescriptor field)
private static Action<object, object?> GetSetMethod(FieldDescriptor field, FieldDescriptor? wrapperTypeValueField)
{
Debug.Assert(!field.IsRepeated && !field.IsMap, "Collections shouldn't have a setter.");

Expand All @@ -135,19 +157,27 @@ private JsonPropertyInfo CreatePropertyInfo(JsonTypeInfo typeInfo, string name,
throw new InvalidOperationException($"Multiple values specified for oneof {field.RealContainingOneof.Name}.");
}

SetFieldValue(field, (IMessage)o, v);
SetFieldValue(field, wrapperTypeValueField, (IMessage)o, v);
};
}

return (o, v) =>
{
SetFieldValue(field, (IMessage)o, v);
SetFieldValue(field, wrapperTypeValueField, (IMessage)o, v);
};

static void SetFieldValue(FieldDescriptor field, IMessage m, object? v)
static void SetFieldValue(FieldDescriptor field, FieldDescriptor? wrapperTypeValueField, IMessage m, object? v)
{
if (v != null)
{
// This field exposes a wrapper type. Need to create a wrapper instance and set the value on it.
if (wrapperTypeValueField != null && v is not IMessage)
{
var wrapper = (IMessage)Activator.CreateInstance(field.MessageType.ClrType)!;
wrapperTypeValueField.Accessor.SetValue(wrapper, v);
v = wrapper;
}

field.Accessor.SetValue(m, v);
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests.Infrastructure;
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests.TestObjects.ProtobufMessages;
using Transcoding;
using Xunit.Abstractions;

Expand Down Expand Up @@ -531,6 +532,25 @@ public void NullableWrappers()
AssertReadJson<HelloRequest.Types.Wrappers>(json);
}

[Fact]
public void NullableWrappers_Type()
{
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<WrappersMessage>(json, serializeOld: false);
Assert.Equal("A string", result.StringValue.Value);
}

[Fact]
public void NullValue_Default_Null()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests.Infrastructure;
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests.TestObjects.ProtobufMessages;
using Transcoding;
using Xunit.Abstractions;
using Type = System.Type;
Expand Down Expand Up @@ -201,6 +202,25 @@ public void NullableWrappers()
AssertWrittenJson(wrappers);
}

[Fact]
public void NullableWrappers_Types()
{
var wrappers = new WrappersMessage
{
BoolValue = new BoolValue { Value = true },
BytesValue = new BytesValue { Value = ByteString.CopyFrom(Encoding.UTF8.GetBytes("Hello world")) },
DoubleValue = new DoubleValue { Value = 1.1 },
FloatValue = new FloatValue { Value = 1.2f },
Int32Value = new Int32Value { Value = 1 },
Int64Value = new Int64Value { Value = 2L },
StringValue = new StringValue { Value = "A string" },
Uint32Value = new UInt32Value { Value = 3U },
Uint64Value = new UInt64Value { Value = 4UL }
};

AssertWrittenJson(wrappers);
}

[Fact]
public void NullableWrapper_Root_Int32()
{
Expand Down
Loading
Loading