Json.Net序列化为自定义转换器提供了错误的属性

时间:2019-02-15 01:27:01

标签: c# json.net

我有一个要使用Json.Net进行序列化和反序列化的模型:

public struct RangeOrValue
{
    public int Value { get; }
    public int Min { get; }
    public int Max { get; }
    public bool IsRange { get; }

    public RangeOrValue(int min, int max)
    {
        Min = min;
        Max = max;
        IsRange = true;
        Value = 0;
    }

    public RangeOrValue(int value)
    {
        Min = 0;
        Max = 0;
        Value = value;
        IsRange = false;
    }
}

我对序列化有特殊要求。如果使用第一个构造函数,则该值应序列化为{ "Min": <min>, "Max": <max> }。 但是,如果使用第二个构造函数,则值应序列化为<value>

例如,new RangeOrValue(0, 10)需要序列化为{ "Min": 0, "Max": 10 }new RangeOrValue(10)需要序列化为10

我编写了这个自定义转换器来完成此任务:

public class RangeOrValueConverter : JsonConverter<RangeOrValue>
{
    public override void WriteJson(JsonWriter writer, RangeOrValue value, JsonSerializer serializer)
    {
        if (value.IsRange)
        {
            // Range values are stored as objects
            writer.WriteStartObject();
            writer.WritePropertyName("Min");
            writer.WriteValue(value.Min);
            writer.WritePropertyName("Max");
            writer.WriteValue(value.Max);
            writer.WriteEndObject();
        }
        else
        {
            writer.WriteValue(value.Value);
        }
    }

    public override RangeOrValue ReadJson(JsonReader reader, Type objectType, RangeOrValue existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        reader.Read();
        // If the type is range, then first token should be property name ("Min" property)
        if (reader.TokenType == JsonToken.PropertyName)
        {
            // Read min value
            int min = reader.ReadAsInt32() ?? 0;
            // Read next property name
            reader.Read(); 
            // Read max value
            int max = reader.ReadAsInt32() ?? 0;
            // Read object end
            reader.Read();
            return new RangeOrValue(min, max);
        }

        // Read simple int
        return new RangeOrValue(Convert.ToInt32(reader.Value));
    }
}

为了测试功能,我编写了一个简单的测试:

[TestFixture]
public class RangeOrValueConverterTest
{
    public class Model
    {
        public string Property1 { get; set; }
        public RangeOrValue Value { get; set; }
        public string Property2 { get; set; }
        public RangeOrValue[] Values { get; set; }
        public string Property3 { get; set; }
    }

    [Test]
    public void Serialization_Value()
    {
        var model = new Model
        {
            Value = new RangeOrValue(10),
            Values = new[] {new RangeOrValue(30), new RangeOrValue(40), new RangeOrValue(50),},
            Property1 = "P1",
            Property2 = "P2",
            Property3 = "P3"
        };

        string json = JsonConvert.SerializeObject(model, new RangeOrValueConverter());

        var deserializedModel = JsonConvert.DeserializeObject<Model>(json, new RangeOrValueConverter());

        Assert.AreEqual(model, deserializedModel);
    }
}

运行测试时,对象成功序列化。但是当它尝试反序列化时,我收到此错误:

Newtonsoft.Json.JsonReaderException : Could not convert string to integer: P2. Path 'Property2', line 1, position 46.

堆栈跟踪引至行int min = reader.ReadAsInt32() ?? 0;

我认为我在转换器中做错了,导致Json.Net向转换器提供错误的值。但我不太清楚。有什么想法吗?

1 个答案:

答案 0 :(得分:1)

您的基本问题是,在ReadJson()的开头,您无条件调用Read()来使读者越过当前令牌:

public override RangeOrValue ReadJson(JsonReader reader, Type objectType, RangeOrValue existingValue, bool hasExistingValue, JsonSerializer serializer)
{
    reader.Read(); 

但是,如果当前令牌是与具有单个值的RangeOrValue相对应的整数,则您刚刚跳过了该值,使读者位于接下来的内容上。相反,当该值的类型为JsonToken.Integer时,您需要处理 current 值。

话虽如此,您的转换器还有其他可能的问题,主要与您假设假设传入的JSON采用特定格式而不是验证这一事实有关。

  • 根据JSON standard,对象是一组无序的名称/值对 ,但是ReadJson()假定特定的属性顺序。

  • ReadJson()不会跳过过去或出现未知属性错误。

  • ReadJson()在截断的文件上不会出错。

  • ReadJson()在意外的令牌类型(例如,数组而不是对象或整数)上不会出错。

  • 如果JSON文件包含注释(JSON标准中未包含注释,但Json.NET支持),则ReadJson()将不会对此进行处理。

  • 转换器不处理Nullable<RangeOrValue>个成员。

    请注意,如果您继承自JsonConverter<T>,则必须为TNullable<T>编写单独的转换器。因此,对于结构体,我认为从基类JsonConverter继承会更容易。

处理这些问题的JsonConverter如下所示:

public class RangeOrValueConverter : JsonConverter
{
    const string MinName = "Min";
    const string MaxName = "Max";

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(RangeOrValue) || Nullable.GetUnderlyingType(objectType) == typeof(RangeOrValue);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var range = (RangeOrValue)value;
        if (range.IsRange)
        {
            // Range values are stored as objects
            writer.WriteStartObject();
            writer.WritePropertyName(MinName);
            writer.WriteValue(range.Min);
            writer.WritePropertyName(MaxName);
            writer.WriteValue(range.Max);
            writer.WriteEndObject();
        }
        else
        {
            writer.WriteValue(range.Value);
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        switch (reader.MoveToContent().TokenType)
        {
            case JsonToken.Null:
                // nullable RangeOrValue; return null.
                return null;  

            case JsonToken.Integer:
                return new RangeOrValue(reader.ValueAsInt32());

            case JsonToken.StartObject:
                int? min = null;
                int? max = null;
                var done = false;
                while (!done)
                {
                    // Read the next token skipping comments if any
                    switch (reader.ReadToContentAndAssert().TokenType)
                    {
                        case JsonToken.PropertyName:
                            var name = (string)reader.Value;
                            if (name.Equals(MinName, StringComparison.OrdinalIgnoreCase))
                                // ReadAsInt32() reads the NEXT token as an Int32, thus advancing past the property name.
                                min = reader.ReadAsInt32();
                            else if (name.Equals(MaxName, StringComparison.OrdinalIgnoreCase))
                                max = reader.ReadAsInt32();
                            else
                                // Unknown property name.  Skip past it and its value.
                                reader.ReadToContentAndAssert().Skip();
                            break;

                        case JsonToken.EndObject:
                            done = true;
                            break;

                        default:
                            throw new JsonSerializationException(string.Format("Invalid token type {0} at path {1}", reader.TokenType, reader.Path));
                    }
                }
                if (max != null && min != null)
                    return new RangeOrValue(min.Value, max.Value);
                throw new JsonSerializationException(string.Format("Missing min or max at path {0}", reader.Path));

            default:
                throw new JsonSerializationException(string.Format("Invalid token type {0} at path {1}", reader.TokenType, reader.Path));
        }
    }
}

使用扩展方法:

public static partial class JsonExtensions
{
    public static int ValueAsInt32(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType != JsonToken.Integer)
            throw new JsonSerializationException("Value is not Int32");
        try
        {
            return Convert.ToInt32(reader.Value, NumberFormatInfo.InvariantInfo);
        }
        catch (Exception ex)
        {
            // Wrap the system exception in a serialization exception.
            throw new JsonSerializationException(string.Format("Invalid integer value {0}", reader.Value), ex);
        }
    }

    public static JsonReader ReadToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        while (reader.Read())
        {
            if (reader.TokenType != JsonToken.Comment)
                return reader;
        }
        throw new JsonReaderException(string.Format("Unexpected end at path {0}", reader.Path));
    }

    public static JsonReader MoveToContent(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)
            if (!reader.Read())
                return reader;
        while (reader.TokenType == JsonToken.Comment && reader.Read())
            ;
        return reader;
    }
}

但是,如果您愿意付出一点性能损失,可以通过对DTO进行序列化和反序列化来简化转换器,如下所示,它使用相同的扩展方法类:

public class RangeOrValueConverter : JsonConverter
{
    class RangeDTO
    {
        public int Min, Max;
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(RangeOrValue) || Nullable.GetUnderlyingType(objectType) == typeof(RangeOrValue);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var range = (RangeOrValue)value;
        if (range.IsRange)
        {
            var dto = new RangeDTO { Min = range.Min, Max = range.Max };
            serializer.Serialize(writer, dto);
        }
        else
        {
            writer.WriteValue(range.Value);
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        switch (reader.MoveToContent().TokenType)
        {
            case JsonToken.Null:
                // nullable RangeOrValue; return null.
                return null;

            case JsonToken.Integer:
                return new RangeOrValue(reader.ValueAsInt32());

            default:
                var dto = serializer.Deserialize<RangeDTO>(reader);
                return new RangeOrValue(dto.Min, dto.Max);
        }
    }
}

演示小提琴,显示了两个转换器here