如何在gRPC拦截器中正确序列化Google.Protobuf.Any为JSON日志
解决gRPC Interceptor中Google.Protobuf.WellKnownTypes.Any类型日志序列化问题
问题根源
System.Text.Json默认无法识别Protobuf的Any类型,会直接将其value字段序列化为字节数组,而不会自动解析为对应的具体消息类型。要生成可读日志,需要手动根据typeUrl将Any解析为具体消息后再序列化。
解决方案步骤
1. 维护Protobuf类型注册表
创建全局字典,映射Any的typeUrl到对应的Protobuf消息类型,方便后续解析时快速查找:
// 根据业务实际情况,添加所有可能被Any包装的Protobuf类型 private static readonly Dictionary<string, Type> ProtobufTypeRegistry = new() { {"type.googleapis.com/CardDeactivation.CardCloseParams", typeof(CardCloseParams)}, {"type.googleapis.com/CardDeactivation.CardInfo", typeof(CardInfo)}, {"type.googleapis.com/CardDeactivation.AccountInfo", typeof(AccountInfo)} };
2. 自定义JsonConverter处理Any类型
实现JsonConverter<Any>,在序列化阶段自动将Any解析为具体消息类型,再输出可读JSON:
public class ProtobufAnyJsonConverter : JsonConverter<Any> { public override Any Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // 日志场景以序列化为主,反序列化可根据需求实现,此处暂不处理 throw new NotImplementedException("反序列化逻辑未实现"); } public override void Write(Utf8JsonWriter writer, Any value, JsonSerializerOptions options) { if (value == null || string.IsNullOrEmpty(value.TypeUrl)) { JsonSerializer.Serialize(writer, value, options); return; } // 根据typeUrl查找对应的Protobuf类型 if (ProtobufTypeRegistry.TryGetValue(value.TypeUrl, out var targetType)) { // 通过反射调用Any.Unpack方法解析为具体消息 var unpackMethod = typeof(Any).GetMethod(nameof(Any.Unpack)).MakeGenericMethod(targetType); var message = unpackMethod.Invoke(value, null); // 将解析后的具体消息序列化为可读JSON JsonSerializer.Serialize(writer, message, targetType, options); } else { // 未找到对应类型时,回退到默认序列化(输出typeUrl和字节数组) JsonSerializer.Serialize(writer, value, options); } } }
3. 更新序列化配置
将自定义转换器加入到原有的DefaultOptionsToLog中:
private static readonly JsonSerializerOptions DefaultOptionsToLog = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, IgnoreNullValues = true, PropertyNameCaseInsensitive = true, Converters = { new ProtobufAnyJsonConverter() } // 新增自定义转换器 };
4. 原Interceptor代码无需修改
你的InterceptCall和ToJsonLog方法保持原样即可,序列化时会自动触发自定义转换器处理Any类型。
效果验证
修改后,日志中的screenParams会被序列化为具体的CardCloseParams结构,示例如下:
GRPC CLIENT REQUEST: Method: /CardDeactivation.ScreenService/GetScreenInfoForOperation {"screenType":2,"screenParams":{"cardToClose":{"cardNumber":"123456xxxx"},"cardsToChoose":[],"accountsToChoose":[],"isLast":false}}
注意事项
- 所有可能被
Any包装的Protobuf类型都需要注册到ProtobufTypeRegistry,否则会回退到字节数组输出 - 若项目中Protobuf类型较多,可通过反射自动扫描程序集中实现
IMessage的类型,自动生成typeUrl映射,避免手动维护 - 如果需要支持反序列化,可自行实现
Read方法,逻辑为:先解析JSON到具体类型,再调用Any.Pack完成包装
内容的提问来源于stack exchange,提问作者MyakishHentai
相关产品推荐
相关产品推荐

