You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用System.Text.Json反序列化.NET标准库不可变类型的方法

System.Text.Json 反序列化 ActivityContext 返回默认值的解决方案

问题描述

使用 System.Text.Json 序列化/反序列化 .NET 内置结构体 System.Diagnostics.ActivityContext 时,序列化输出正常,但反序列化得到的实例所有属性均为默认值:

  • 按照官方不可变类型序列化规则,需要给类型构造函数添加[JsonConstructor]特性才能正确反序列化,但ActivityContext是标准库内置类型,无法直接修改源码添加特性
  • 已单独为结构体的ActivityTraceId、ActivitySpanId属性类型编写了自定义JSON转换器,仍然无法解决反序列化问题
  • 场景为测试OpenTelemetry上下文传播的简单JSON序列化逻辑,非生产业务问题

测试代码

using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;

ActivityContext testContext = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None);
Console.WriteLine(testContext.TraceId);

string? serializedActivityContext = JsonSerializer.Serialize(testContext, new JsonSerializerOptions()
{
    Converters = { new ActivityTraceIdJsonConverter(), new ActivitySpanIdJsonConverter()}
});

Console.WriteLine(serializedActivityContext);

ActivityContext deserializedActivityContext = JsonSerializer.Deserialize<ActivityContext>(serializedActivityContext, new JsonSerializerOptions()
{
    Converters = { new ActivityTraceIdJsonConverter(), new ActivitySpanIdJsonConverter()}
});

Console.WriteLine(deserializedActivityContext.TraceId);

public class ActivityTraceIdJsonConverter : JsonConverter<ActivityTraceId>
{
    public override ActivityTraceId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => ActivityTraceId.CreateFromString(reader.GetString());
    public override void Write(Utf8JsonWriter writer, ActivityTraceId value, JsonSerializerOptions options)
        => writer.WriteStringValue(value.ToString());
}
public class ActivitySpanIdJsonConverter : JsonConverter<ActivitySpanId>
{
    public override ActivitySpanId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => ActivitySpanId.CreateFromString(reader.GetString());
    public override void Write(Utf8JsonWriter writer, ActivitySpanId value, JsonSerializerOptions options)
        => writer.WriteStringValue(value.ToString());
}

测试输出

5ac8dcec05989c8d455955f73ecb2560
{"TraceId":"5ac8dcec05989c8d455955f73ecb2560","SpanId":"96d3440ed8eff463","TraceFlags":0,"TraceState":null,"IsRemote":false}
00000000000000000000000000000000

问题原因

  • 仅为属性的类型编写自定义转换器,无法解决ActivityContext本身的构造逻辑问题
  • ActivityContext是没有公开无参构造函数的不可变值类型,源码未标注[JsonConstructor],System.Text.Json 反序列化时无法匹配对应的参数化构造函数完成属性赋值,最终直接返回结构体的默认值,导致所有属性都是0/空的默认状态
  • 对于无法修改源码的内置类型,不需要强行给类型加特性,直接为该类型编写专属自定义JSON转换器即可解决问题。

实现方案

编写 ActivityContext 专属转换器

不需要删除已有的ActivityTraceIdJsonConverter和ActivitySpanIdJsonConverter,新增如下针对ActivityContext的转换器即可,转换器内部会自动复用已注册的子类型转换器:

public class ActivityContextJsonConverter : JsonConverter<ActivityContext>
{
    public override ActivityContext Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException("预期为JSON对象起始标记");

        ActivityTraceId traceId = default;
        ActivitySpanId spanId = default;
        ActivityTraceFlags traceFlags = ActivityTraceFlags.None;
        string? traceState = null;
        bool isRemote = false;

        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
            {
                // 读取完所有属性后,调用公开构造函数返回实例
                return new ActivityContext(traceId, spanId, traceFlags, traceState, isRemote);
            }

            if (reader.TokenType != JsonTokenType.PropertyName)
                throw new JsonException("预期为属性名");

            string propName = reader.GetString()!;
            reader.Read();

            switch (propName)
            {
                case nameof(ActivityContext.TraceId):
                    traceId = JsonSerializer.Deserialize<ActivityTraceId>(ref reader, options);
                    break;
                case nameof(ActivityContext.SpanId):
                    spanId = JsonSerializer.Deserialize<ActivitySpanId>(ref reader, options);
                    break;
                case nameof(ActivityContext.TraceFlags):
                    traceFlags = (ActivityTraceFlags)reader.GetInt32();
                    break;
                case nameof(ActivityContext.TraceState):
                    traceState = reader.TokenType == JsonTokenType.Null ? null : reader.GetString();
                    break;
                case nameof(ActivityContext.IsRemote):
                    isRemote = reader.GetBoolean();
                    break;
                default:
                    reader.Skip();
                    break;
            }
        }

        throw new JsonException("JSON格式不完整,缺少对象结束标记");
    }

    public override void Write(Utf8JsonWriter writer, ActivityContext value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        
        writer.WritePropertyName(nameof(ActivityContext.TraceId));
        JsonSerializer.Serialize(writer, value.TraceId, options);
        
        writer.WritePropertyName(nameof(ActivityContext.SpanId));
        JsonSerializer.Serialize(writer, value.SpanId, options);
        
        writer.WriteNumber(nameof(ActivityContext.TraceFlags), (int)value.TraceFlags);
        writer.WriteString(nameof(ActivityContext.TraceState), value.TraceState);
        writer.WriteBoolean(nameof(ActivityContext.IsRemote), value.IsRemote);
        
        writer.WriteEndObject();
    }
}

使用方式

将新的转换器加入JsonSerializerOptions的Converters列表即可:

var options = new JsonSerializerOptions()
{
    Converters = { 
        new ActivityTraceIdJsonConverter(), 
        new ActivitySpanIdJsonConverter(),
        new ActivityContextJsonConverter() // 新增的转换器
    }
};

简化技巧:如果觉得逐属性读取JSON繁琐,也可以在转换器的Read方法中先将JSON反序列化为一个临时的普通POCO类(所有属性带public set,结构和ActivityContext一致),读取完POCO后再把POCO的属性传入ActivityContext的构造函数生成正式实例,代码会更简洁。


内容的提问来源于stack exchange,提问作者Algirdas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 11:18:25