如何根据json的结构选择要在运行时反序列化的类型? [英] How can I choose what type to deserialize at runtime based on the structure of the json?

查看:154
本文介绍了如何根据json的结构选择要在运行时反序列化的类型?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一些数据存储为Json.数据中的一个属性是整数(旧式数据),如下所示:

"Difficulty": 2,

或完整的对象(新版本):

"Difficulty": {
      "$id": "625",
      "CombatModifier": 2,
      "Name": "Normal",
      "StartingFunds": {
        "$id": "626",
        "Value": 2000.0
      },
      "Dwarves": [
        "Miner",
        "Miner",
        "Miner",
        "Crafter"
      ]
    },

我正在尝试为允许两种版本反序列化的类型编写自定义转换器.

这是C#,使用最新版本的newtonsoft.json.

我已经编写了一个转换器,对整数格式进行反序列化是微不足道的-只是混合给我带来了麻烦.我认为可以检查的唯一方法是尝试失败.但这似乎使阅读器处于不可恢复的状态.另外,在catch块中调用反序列化会导致无限循环.

public class DifficultyConverter : JsonConverter
    {
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            try
            {
                var jObject = serializer.Deserialize<JValue>(reader);
                if (jObject.Value is Int32 intv)
                    return Library.EnumerateDifficulties().FirstOrDefault(d => d.CombatModifier == intv);
                else
                    return null;
            }
            catch (Exception e)
            {
                return serializer.Deserialize<Difficulty>(reader);
            }
        }                

        public override bool CanWrite
        {
            get { return false; }
        }

        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(Difficulty);
        }
    }

理想情况下,我将能够始终序列化为新格式,并且仍然支持读取这两种格式.其他几个选项包括:

  • 创建另一个不包含自定义转换器的序列化器对象,并从catch块中调用它.
  • 在加载时检测过期文件并修改文本,然后再尝试反序列化.

想要避免那些事情.

解决方案

您在这里遇到了两个问题:

  1. 在调用ReadJson()时将获得无限递归,因为您的转换器已通过用于设置嵌套反序列化的序列化器进行注册,可以通过设置或直接将[JsonConverter(typeof(DifficultyConverter))]应用于Difficulty来进行嵌套反序列化.

    避免这种情况的标准解决方案是手动分配Difficulty,然后使用此答案 使用JsonConverter进行Json.NET自定义序列化-如何获得默认"行为 )-但您也是使用 PreserveReferencesHandling.Objects ,这种方法不起作用. /p>

    所做的与参考保存一起使用的是采用从此答案 使用[JsonConvert()] 时,JSON.Net抛出StackOverflowException并反序列化为包含类型为Difficulty的属性,该属性具有直接应用于该属性的替代转换器.

  2. serializer.Deserialize<JValue>(reader);可以使读取器超越当前令牌.这将导致以后尝试反序列化为对象失败.

    相反,只需检查 JsonReader.TokenType 或预加载到JToken并检查 Type .

将以上内容放在一起,您的转换器应如下所示:

public class DifficultyConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var token = JToken.Load(reader);

        switch (token.Type)
        {
            case JTokenType.Null:
                return null;

            case JTokenType.Integer:
                {
                    var intv = (int)token;
                    return Library.EnumerateDifficulties().FirstOrDefault(d => d.CombatModifier == intv);
                }

            case JTokenType.Object:
                return token.DefaultToObject(objectType, serializer);

            default:
                throw new JsonSerializationException(string.Format("Unknown token {0}", token.Type));
        }
    }                

    public override bool CanWrite => false;

    public override bool CanConvert(Type objectType) => objectType == typeof(Difficulty);
}

使用以下扩展方法:

public static partial class JsonExtensions
{
    public static object DefaultToObject(this JToken token, Type type, JsonSerializer serializer = null)
    {
        var oldParent = token.Parent;

        var dtoToken = new JObject(new JProperty(nameof(DefaultSerializationDTO<object>.Value), token));
        var dtoType = typeof(DefaultSerializationDTO<>).MakeGenericType(type);
        var dto = (IHasValue)(serializer ?? JsonSerializer.CreateDefault()).Deserialize(dtoToken.CreateReader(), dtoType);

        if (oldParent == null)
            token.RemoveFromLowestPossibleParent();

        return dto == null ? null : dto.GetValue();
    }

    public static JToken RemoveFromLowestPossibleParent(this JToken node)
    {
        if (node == null)
            return null;
        // If the parent is a JProperty, remove that instead of the token itself.
        var contained = node.Parent is JProperty ? node.Parent : node;
        contained.Remove();
        // Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
        if (contained is JProperty)
            ((JProperty)node.Parent).Value = null;
        return node;
    }

    interface IHasValue
    {
        object GetValue();
    }

    [JsonObject(NamingStrategyType = typeof(Newtonsoft.Json.Serialization.DefaultNamingStrategy), IsReference = false)]
    class DefaultSerializationDTO<T> : IHasValue
    {
        public DefaultSerializationDTO(T value) { this.Value = value; }

        public DefaultSerializationDTO() { }

        [JsonConverter(typeof(NoConverter)), JsonProperty(ReferenceLoopHandling = ReferenceLoopHandling.Serialize)]
        public T Value { get; set; }

        public object GetValue() => Value;
    }
}

public class NoConverter : JsonConverter
{
    // NoConverter taken from this answer https://stackoverflow.com/a/39739105/3744182
    // To https://stackoverflow.com/questions/39738714/selectively-use-default-json-converter
    // By https://stackoverflow.com/users/3744182/dbc
    public override bool CanConvert(Type objectType)  { throw new NotImplementedException(); /* This converter should only be applied via attributes */ }

    public override bool CanRead { get { return false; } }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException(); }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); }
}

演示小提琴此处.

I have some data stored as Json. One property in the data is either an integer (legacy data) like so:

"Difficulty": 2,

Or a complete object (new versions):

"Difficulty": {
      "$id": "625",
      "CombatModifier": 2,
      "Name": "Normal",
      "StartingFunds": {
        "$id": "626",
        "Value": 2000.0
      },
      "Dwarves": [
        "Miner",
        "Miner",
        "Miner",
        "Crafter"
      ]
    },

I am trying to write a custom converter for the type that allows deserialization of both versions.

This is C#, using the latest version of newtonsoft.json.

I've written a converter, and deserializing the integer format is trivial - it's only the mix that is causing me trouble. The only way I can think to check is to try and fail; but this appears to leave the reader in an unrecoverable state. Also, calling deserialize in the catch block leads to an infinite loop.

public class DifficultyConverter : JsonConverter
    {
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            try
            {
                var jObject = serializer.Deserialize<JValue>(reader);
                if (jObject.Value is Int32 intv)
                    return Library.EnumerateDifficulties().FirstOrDefault(d => d.CombatModifier == intv);
                else
                    return null;
            }
            catch (Exception e)
            {
                return serializer.Deserialize<Difficulty>(reader);
            }
        }                

        public override bool CanWrite
        {
            get { return false; }
        }

        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(Difficulty);
        }
    }

Ideally I would be able to serialize into the new format always, and still support reading both formats. A couple of other options include:

  • Creating another serializer object that does not include the custom converter and calling it from the catch block.
  • Detecting out of date files at load and modifying the text before attempting to deserialize.

Kind of want to avoid those tho.

解决方案

You have a couple of problems here:

  1. You are getting an infinite recursion in calls to ReadJson() because your converter is registered with the serializer you are using to do the nested deserialization, either through settings or by directly applying [JsonConverter(typeof(DifficultyConverter))] to Difficulty.

    The standard solution to avoid this is to manually allocate your Difficulty and then use serializer.Populate() to deserialize its members (e.g. as shown in this answer to Json.NET custom serialization with JsonConverter - how to get the "default" behavior) -- but you are also using PreserveReferencesHandling.Objects, which does not work with this approach.

    What does work with reference preservation is to adopt the approach from this answer to JSON.Net throws StackOverflowException when using [JsonConvert()] and deserialize to some DTO that contains a property of type Difficulty which has a superseding converter applied directly to the property.

  2. serializer.Deserialize<JValue>(reader); may advance the reader past the current token. This will cause the later attempt to deserialize as an object to fail.

    Instead, just check the JsonReader.TokenType or preload into a JToken and check the Type.

Putting the above together, your converter should look like the following:

public class DifficultyConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var token = JToken.Load(reader);

        switch (token.Type)
        {
            case JTokenType.Null:
                return null;

            case JTokenType.Integer:
                {
                    var intv = (int)token;
                    return Library.EnumerateDifficulties().FirstOrDefault(d => d.CombatModifier == intv);
                }

            case JTokenType.Object:
                return token.DefaultToObject(objectType, serializer);

            default:
                throw new JsonSerializationException(string.Format("Unknown token {0}", token.Type));
        }
    }                

    public override bool CanWrite => false;

    public override bool CanConvert(Type objectType) => objectType == typeof(Difficulty);
}

Using the following extension methods:

public static partial class JsonExtensions
{
    public static object DefaultToObject(this JToken token, Type type, JsonSerializer serializer = null)
    {
        var oldParent = token.Parent;

        var dtoToken = new JObject(new JProperty(nameof(DefaultSerializationDTO<object>.Value), token));
        var dtoType = typeof(DefaultSerializationDTO<>).MakeGenericType(type);
        var dto = (IHasValue)(serializer ?? JsonSerializer.CreateDefault()).Deserialize(dtoToken.CreateReader(), dtoType);

        if (oldParent == null)
            token.RemoveFromLowestPossibleParent();

        return dto == null ? null : dto.GetValue();
    }

    public static JToken RemoveFromLowestPossibleParent(this JToken node)
    {
        if (node == null)
            return null;
        // If the parent is a JProperty, remove that instead of the token itself.
        var contained = node.Parent is JProperty ? node.Parent : node;
        contained.Remove();
        // Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
        if (contained is JProperty)
            ((JProperty)node.Parent).Value = null;
        return node;
    }

    interface IHasValue
    {
        object GetValue();
    }

    [JsonObject(NamingStrategyType = typeof(Newtonsoft.Json.Serialization.DefaultNamingStrategy), IsReference = false)]
    class DefaultSerializationDTO<T> : IHasValue
    {
        public DefaultSerializationDTO(T value) { this.Value = value; }

        public DefaultSerializationDTO() { }

        [JsonConverter(typeof(NoConverter)), JsonProperty(ReferenceLoopHandling = ReferenceLoopHandling.Serialize)]
        public T Value { get; set; }

        public object GetValue() => Value;
    }
}

public class NoConverter : JsonConverter
{
    // NoConverter taken from this answer https://stackoverflow.com/a/39739105/3744182
    // To https://stackoverflow.com/questions/39738714/selectively-use-default-json-converter
    // By https://stackoverflow.com/users/3744182/dbc
    public override bool CanConvert(Type objectType)  { throw new NotImplementedException(); /* This converter should only be applied via attributes */ }

    public override bool CanRead { get { return false; } }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException(); }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); }
}

Demo fiddle here.

这篇关于如何根据json的结构选择要在运行时反序列化的类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆