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

System.Text.Json反序列化Expr类object属性始终得JsonElement的解决办法

问题描述

我定义了一个表示表达式树的Expr类,包含三个属性:Left(可存储嵌套Expr对象或离散值)、Operator(操作类型枚举)、Right(可存储嵌套Expr对象或离散值),代码如下:

public class Expr
{
  public object Left { get; set; }
  public OperatorEnum Operator { get; set; }
  public object Right { get; set; }
}

使用System.Text.Json进行反序列化时,无论Left和Right属性存储的是嵌套Expr对象,还是string、decimal等离散值,最终得到的实例中这两个属性始终为JsonElement类型。例如反序列化{ "Left": "foo", "Operator": "Equals", "Right": "bar" }后,expr.Left.GetType()返回的始终是JsonElement。

我已编写了一个JsonConverter类,并将其添加到JsonSerializerOptions中用于序列化和反序列化,但相关经验不足,代码如下:

namespace Test
{
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Text.Json;
    using System.Text.Json.Serialization;

    public class ExpressionConverter : JsonConverter<Expr>
    {
        public override Expr Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            Expr ret = new Expr();

            bool inLeft = false;
            bool inOper = false;
            bool inRight = false;

            while (reader.Read())
            {
                if (!inLeft && !inOper && !inRight)
                {
                    if (reader.TokenType == JsonTokenType.PropertyName)
                    {
                        if (reader.ValueTextEquals("Left"))
                        {
                            inLeft = true;
                        }
                        else if (reader.ValueTextEquals("Operator"))
                        {
                            inOper = true;
                        }
                        else if (reader.ValueTextEquals("Right"))
                        {
                            inRight = true;
                        }
                    }
                }
                else
                {
                    if (inLeft)
                    { 
                        if (reader.TokenType == JsonTokenType.String)
                        {
                            string val = reader.GetString();
                            if (val != null) ret.Left = val.ToString();
                            else ret.Left = null;
                        }
                        else if (reader.TokenType == JsonTokenType.StartObject)
                        {
                            ret.Left = JsonSerializer.Deserialize<Expr>(ref reader, options);
                        }
                        else if (reader.TokenType == JsonTokenType.StartArray)
                        {
                            ret.Left = JsonSerializer.Deserialize<List<object>>(ref reader, options);
                        }
                        else if (reader.TokenType == JsonTokenType.Number)
                        {
                            ret.Left = reader.GetDecimal();
                        }
                        else if (reader.TokenType == JsonTokenType.True)
                        {
                            ret.Left = true;
                        }
                        else if (reader.TokenType == JsonTokenType.False)
                        {
                            ret.Left = false;
                        }
                        else
                        {
                            throw new InvalidOperationException("Unexpected token type '" + reader.TokenType.ToString() + "' while processing value for 'Left' expression.");
                        }
                    }
                    else if (inOper)
                    {
                        ret.Operator = (OperatorEnum)(Enum.Parse(typeof(OperatorEnum), reader.GetString()));
                    }
                    else if (inRight)
                    {
                        if (reader.TokenType == JsonTokenType.String)
                        {
                            string val = reader.GetString();
                            if (val != null) ret.Right = val.ToString();
                            else ret.Left = null;
                        }
                        else if (reader.TokenType == JsonTokenType.StartObject)
                        {
                            ret.Right = JsonSerializer.Deserialize<Expr>(ref reader, options);
                        }
                        else if (reader.TokenType == JsonTokenType.StartArray)
                        {
                            ret.Right = JsonSerializer.Deserialize<List<object>>(ref reader, options);
                        }
                        else if (reader.TokenType == JsonTokenType.Number)
                        {
                            ret.Right = reader.GetDecimal();
                        }
                        else if (reader.TokenType == JsonTokenType.True)
                        {
                            ret.Right = true;
                        }
                        else if (reader.TokenType == JsonTokenType.False)
                        {
                            ret.Right = false;
                        }
                        else
                        {
                            throw new InvalidOperationException("Unexpected token type '" + reader.TokenType.ToString() + "' while processing value for 'Left' expression.");
                        }
                    }                        

                    inLeft = false;
                    inOper = false;
                    inRight = false;
                }
            }

            return ret;
        }

        public override void Write(Utf8JsonWriter writer, Expr value, JsonSerializerOptions options)
        {
            if (value == null) throw new ArgumentNullException(nameof(value));

            writer.WriteStartObject();

            if (value.Left is Expr)
            {
                writer.WritePropertyName("Left");
                JsonSerializer.Serialize(writer, value.Left, options);
            }
            else if (IsNumberType(value.Left))
            {
                writer.WriteNumber("Left", Convert.ToDecimal(value.Left));
            }
            else if (IsBooleanType(value.Left))
            {
                writer.WriteBoolean("Left", Convert.ToBoolean(value.Left));
            }
            else
            {
                if (value.Left != null)
                {
                    writer.WriteString("Left", value.Left.ToString());
                }
                else
                {
                    writer.WriteNull("Left");
                }
            }

            writer.WriteString("Operator", value.Operator.ToString());

            if (value.Right is Expr)
            {
                writer.WritePropertyName("Right");
                JsonSerializer.Serialize(writer, value.Right, options);
            }
            else if (IsNumberType(value.Right))
            {
                writer.WriteNumber("Right", Convert.ToDecimal(value.Right));
            }
            else if (IsBooleanType(value.Right))
            {
                writer.WriteBoolean("Right", Convert.ToBoolean(value.Right));
            }
            else
            {
                if (value.Right != null)
                {
                    writer.WriteString("Right", value.Right.ToString());
                }
                else
                {
                    writer.WriteNull("Right");
                }
            }

            writer.WriteEndObject();
        }

        private bool IsNumberType(object value)
        {
            if (value == null) return false;
            return Double.TryParse(value.ToString(), out double _);
        }

        private bool IsBooleanType(object value)
        {
            if (value == null) return false;
            return Boolean.TryParse(value.ToString(), out bool _);
        }
    }
}

请问有没有办法让反序列化后的Left和Right属性正确反映实际值类型(如Expr、string等)?


解决方案

你的转换器思路正确,但存在几个关键问题导致类型转换不符合预期,以下是修正后的完整实现:

核心修正点

  • 修复Right属性反序列化bug:原代码处理Right为null时错误赋值给Left,且异常信息错误指向Left。
  • 新增Null值处理:原代码未显式处理JsonTokenType.Null,导致null值无法正确赋值。
  • 优化枚举解析:添加大小写不敏感解析逻辑,并处理无效枚举值的异常情况。
  • 递归处理数组元素:原数组反序列化List<object>会导致内部元素仍为JsonElement,需递归解析每个数组元素。
  • 改进类型判断逻辑:原IsNumberType和IsBooleanType通过字符串解析判断类型不准确,改为直接判断对象实际类型。

修正后的转换器代码

namespace Test
{
    using System;
    using System.Collections.Generic;
    using System.Text.Json;
    using System.Text.Json.Serialization;

    public class ExpressionConverter : JsonConverter<Expr>
    {
        public override Expr Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
                throw new JsonException("Expected start of object for Expr");

            var expr = new Expr();

            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndObject)
                    return expr;

                if (reader.TokenType != JsonTokenType.PropertyName)
                    throw new JsonException("Expected property name");

                var propName = reader.GetString();
                reader.Read(); // 移动到属性值位置

                switch (propName)
                {
                    case "Left":
                        expr.Left = ReadValue(ref reader, options);
                        break;
                    case "Operator":
                        if (!Enum.TryParse<OperatorEnum>(reader.GetString(), ignoreCase: true, out var op))
                            throw new JsonException($"Invalid Operator value: {reader.GetString()}");
                        expr.Operator = op;
                        break;
                    case "Right":
                        expr.Right = ReadValue(ref reader, options);
                        break;
                    default:
                        reader.Skip(); // 跳过未知属性,避免解析失败
                        break;
                }
            }

            throw new JsonException("Unexpected end of JSON while parsing Expr");
        }

        public override void Write(Utf8JsonWriter writer, Expr value, JsonSerializerOptions options)
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));

            writer.WriteStartObject();

            WriteValue(writer, "Left", value.Left, options);
            writer.WriteString("Operator", value.Operator.ToString());
            WriteValue(writer, "Right", value.Right, options);

            writer.WriteEndObject();
        }

        // 递归读取值,支持嵌套Expr、基础类型、数组
        private object ReadValue(ref Utf8JsonReader reader, JsonSerializerOptions options)
        {
            return reader.TokenType switch
            {
                JsonTokenType.StartObject => JsonSerializer.Deserialize<Expr>(ref reader, options),
                JsonTokenType.StartArray => ReadArray(ref reader, options),
                JsonTokenType.String => reader.GetString(),
                JsonTokenType.Number => reader.GetDecimal(),
                JsonTokenType.True => true,
                JsonTokenType.False => false,
                JsonTokenType.Null => null,
                _ => throw new JsonException($"Unsupported token type: {reader.TokenType}")
            };
        }

        // 递归解析数组内的每个元素
        private List<object> ReadArray(ref Utf8JsonReader reader, JsonSerializerOptions options)
        {
            var list = new List<object>();
            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndArray)
                    return list;

                list.Add(ReadValue(ref reader, options));
            }
            throw new JsonException("Unexpected end of JSON while parsing array");
        }

        // 根据实际类型选择对应的JSON写入方法
        private void WriteValue(Utf8JsonWriter writer, string propertyName, object value, JsonSerializerOptions options)
        {
            if (value == null)
            {
                writer.WriteNull(propertyName);
                return;
            }

            if (!string.IsNullOrEmpty(propertyName))
                writer.WritePropertyName(propertyName);

            switch (value)
            {
                case Expr expr:
                    JsonSerializer.Serialize(writer, expr, options);
                    break;
                case decimal d:
                    writer.WriteNumberValue(d);
                    break;
                case int i:
                    writer.WriteNumberValue(i);
                    break;
                case bool b:
                    writer.WriteBooleanValue(b);
                    break;
                case string s:
                    writer.WriteStringValue(s);
                    break;
                case IEnumerable<object> array:
                    writer.WriteStartArray();
                    foreach (var item in array)
                        WriteValue(writer, null, item, options); // 数组元素无需属性名
                    writer.WriteEndArray();
                    break;
                default:
                    writer.WriteStringValue(value.ToString());
                    break;
            }
        }
    }
}

使用示例

确保序列化/反序列化时将转换器添加到JsonSerializerOptions:

var options = new JsonSerializerOptions();
options.Converters.Add(new ExpressionConverter());

// 基础类型反序列化
var basicJson = "{ \"Left\": \"foo\", \"Operator\": \"Equals\", \"Right\": \"bar\" }";
var basicExpr = JsonSerializer.Deserialize<Expr>(basicJson, options);
// basicExpr.Left 类型为 string,basicExpr.Right 类型为 string

// 嵌套Expr反序列化
var nestedJson = "{ \"Left\": { \"Left\": 123, \"Operator\": \"GreaterThan\", \"Right\": 45 }, \"Operator\": \"And\", \"Right\": true }";
var nestedExpr = JsonSerializer.Deserialize<Expr>(nestedJson, options);
// nestedExpr.Left 类型为 Expr,其 Left 为 decimal,Right 为 decimal;nestedExpr.Right 类型为 bool

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 22:30:55