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

System.Text.Json反序列化自定义只读结构体失败问题

问题根因

System.Text.Json 内置序列化器不会自动识别自定义只读结构体的静态实例映射、隐式字符串转换逻辑。未注册自定义转换器时,无法将JSON中的字符串值映射到对应结构体实例,就会触发类型转换异常。该方案完全保留原有Side结构体的只读设计,不需要将其修改为枚举类型。

实现步骤

1. 编写适配System.Text.Json的自定义转换器

对齐提供的Json.NET实现逻辑,支持精确匹配+大小写不敏感匹配、支持可空值类型、序列化自动输出对应字符串,先写泛型基类:

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

public abstract class StjStringStructConverter<T> : JsonConverter<T> where T : struct
{
    protected abstract IReadOnlyList<(T Instance, string JsonValue)> Mapping { get; }
    private readonly bool _enableCaseInsensitiveMatch;

    protected StjStringStructConverter(bool enableCaseInsensitiveMatch = true)
    {
        _enableCaseInsensitiveMatch = enableCaseInsensitiveMatch;
    }

    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.Null) return default;
        if (reader.TokenType != JsonTokenType.String)
            throw new JsonException($"Token type mismatch when parsing {typeof(T).Name}, expect string but get {reader.TokenType}");

        var rawValue = reader.GetString();
        if (string.IsNullOrWhiteSpace(rawValue)) return default;

        var matched = Mapping.FirstOrDefault(item => item.JsonValue.Equals(rawValue, StringComparison.Ordinal));
        if (matched.Equals(default) && _enableCaseInsensitiveMatch)
        {
            matched = Mapping.FirstOrDefault(item => item.JsonValue.Equals(rawValue, StringComparison.OrdinalIgnoreCase));
        }

        if (matched.Equals(default))
        {
            Console.WriteLine($"[Warning] Failed to map {typeof(T).Name} value: {rawValue}, valid values: {string.Join(", ", Mapping.Select(i => i.JsonValue))}");
            return default;
        }

        return matched.Instance;
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        var matched = Mapping.FirstOrDefault(item => item.Instance.Equals(value));
        if (matched.Equals(default))
        {
            writer.WriteNullValue();
            return;
        }
        writer.WriteStringValue(matched.JsonValue);
    }

    public override bool HandleNull => true;
}

再编写Side类型专属的转换器,配置映射关系:

public class SideJsonConverter : StjStringStructConverter<Side>
{
    protected override IReadOnlyList<(Side Instance, string JsonValue)> Mapping => new List<(Side, string)>
    {
        (Side.Buy, "BUY"),
        (Side.Sell, "SELL")
    };
}

2. 注册转换器

两种方式二选一即可:

  • 全局注册:所有使用Side类型的序列化/反序列化场景自动生效
var serializerOptions = new JsonSerializerOptions
{
    NumberHandling = JsonNumberHandling.AllowReadingFromString,
    Converters = { new SideJsonConverter() }
};
// 反序列化时传入配置即可正常运行
var deserialized = JsonSerializer.Deserialize<BinanceStreamOrderUpdate>(json, serializerOptions);
  • 属性标记:仅对指定属性生效,无需全局配置
public record BinanceStreamOrderUpdate
{
    [JsonPropertyName("S")]
    [JsonConverter(typeof(SideJsonConverter))]
    public Side Side { get; init; }

    [JsonPropertyName("q")] public decimal Quantity { get; init; }
}
效果说明
  • 原有Side只读结构体的代码完全不需要修改,不需要开放构造函数、不需要改成枚举、不需要调整现有隐式转换等逻辑
  • 反序列化时JSON中的"S":"BUY"/"S":"SELL"会正确映射到对应的静态Side实例,序列化时也会自动输出标准的大写字符串值
  • 匹配逻辑和提供的Json.NET实现完全对齐,遇到未知值默认输出警告不中断流程,如果需要严格校验,把匹配失败分支的日志输出替换为抛出JsonException即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 02:54:20