我该如何使用Json.NET来按引用反序列化对象,在使用自定义格式的引用时?

8
我有以下格式的JSON数据:

{
    "users": [
        {
            "first_name": "John",
            "last_name": "Smith",
            "vet": [ "FOO", "VET-1" ],
            "animals": [ [ "FOO", "ANIMAL-22" ] ]
        },
        {
            "first_name": "Susan",
            "last_name": "Smith",
            "vet": [ "FOO", "VET-1" ]
        }
    ],
    "BAR": {
        "VET-1": {
            "vet_name": "Acme, Inc",
            "vet_id": 456
        },
        "ANIMAL-22": {
            "animal_name": "Fido",
            "species": "dog",
            "animal_id": 789,
            "vet": [ "FOO", "VET-1" ]
        }
    }
}

一些嵌套对象或被多次引用的对象将被序列化为引用。

这些被引用的对象随后会包含在JSON对象末尾的BAR数组中,并通过[ "FOO", "ANIMAL-22" ]数组在原处标识。

(FOOBAR都是静态常量,而ANIMAL-22/VET-1标识符则是半随机的)

很遗憾,这与Json.NET已经实现的序列化/反序列化引用对象的方式不匹配,我可以实现的IReferenceResolver似乎无法充分调整行为(它被固定使用"$ref"作为开始)。 我还尝试编写了一个自定义JsonConverter来处理受影响的属性,但是我似乎无法获取对根对象的BAR属性的引用。

有没有办法覆盖Json.NET,将上面的JSON反序列化为这种C#类结构?

public class User
{
    [JsonProperty("first_name")]
    public string FirstName { get; set; }

    [JsonProperty("last_name")]
    public string LastName { get; set; }

    [JsonProperty("vet")]
    public Vet Vet { get; set; }

    [JsonProperty("animals")]
    public List<Animal> Animals { get; set; }
}

public class Vet
{
    [JsonProperty("vet_id")]
    public int Id { get; set; }

    [JsonProperty("vet_name")]
    public string Name { get; set; }
}

public class Animal
{
    [JsonProperty("animal_id")]
    public int Id { get; set; }

    [JsonProperty("animal_name")]
    public string Name { get; set; }

    [JsonProperty("vet")]
    public Vet Vet { get; set; }

    [JsonProperty("species")]
    public string Species { get; set; }
}

编辑 #1:尽管我在示例中只提供了AnimalVet,但是有很多类型以这种方式引用,并且我认为我需要一个“通用”或类型不可知的解决方案,可以处理任何出现[ "FOO", "..." ]数组结构的情况,而不需要为每个C#类型单独编写代码。

1
不,你不能自动实现这个。暂且不谈硬编码"$ref"属性名称,Json.NET是一个单通序列化器,但是"ANIMAL-22""VET-1"看起来是对稍后在文件中出现的JSON对象的前向引用,因此在读取引用时还没有被读取。您需要在反序列化后手动后处理对象以修复此类引用,或者使用LINQ to JSON将JSON预处理为Json.NET所需的格式。 - dbc
1个回答

6

如@dbc在评论中所说,Json.Net没有简单的方法来自动处理您的自定义引用格式。 尽管如此,您可以使用LINQ-to-JSON(JObjects)解析JSON,并借助JsonConverter和一些字典来解析引用和填充您的类,同时仍然让大部分重负留给Json.Net。 这是我会采取的方法:

  1. Create a custom generic JsonConverter which can decode the [ "FOO", "<key>" ] reference format and return the corresponding object from a provided dictionary. Here is the code for the converter:

    public class ReferenceConverter<T> : JsonConverter
    {
        private Dictionary<string, T> ReferenceDict { get; set; }
    
        public ReferenceConverter(Dictionary<string, T> referenceDict)
        {
            ReferenceDict = referenceDict;
        }
    
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(T);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            JArray array = JArray.Load(reader);
            if (array.Count == 2 && 
                array[0].Type == JTokenType.String && 
                (string)array[0] == "FOO" && 
                array[1].Type == JTokenType.String)
            {
                string key = (string)array[1];
                T obj;
                if (ReferenceDict.TryGetValue(key, out obj))
                    return obj;
    
                throw new JsonSerializationException("No " + typeof(T).Name + " was found with the key \"" + key + "\".");
            }
    
            throw new JsonSerializationException("Reference had an invalid format: " + array.ToString());
        }
    
        public override bool CanWrite
        {
            get { return false; }
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    
  2. Parse the JSON into a JObject and build a dictionary of Vets from the BAR section of the JSON.

    JObject data = JObject.Parse(json);
    
    Dictionary<string, Vet> vets = data["BAR"]
        .Children<JProperty>()
        .Where(jp => jp.Value["vet_id"] != null)
        .ToDictionary(jp => jp.Name, jp => jp.Value.ToObject<Vet>());
    
  3. Build a dictionary of Animals from the BAR section of the JSON, using the ReferenceConverter<T> and the dictionary from step 2 to resolve Vet references.

    JsonSerializer serializer = new JsonSerializer();
    serializer.Converters.Add(new ReferenceConverter<Vet>(vets));
    
    Dictionary<string, Animal> animals = data["BAR"]
        .Children<JProperty>()
        .Where(jp => jp.Value["animal_id"] != null)
        .ToDictionary(jp => jp.Name, jp => jp.Value.ToObject<Animal>(serializer));
    
  4. Finally, deserialize the users list from the JSON, again using the ReferenceConverter<T> plus the two dictionaries (so actually two converter instances now, one per dictionary) to resolve all the references.

    serializer.Converters.Add(new ReferenceConverter<Animal>(animals));
    List<User> users = data["users"].ToObject<List<User>>(serializer);
    
完整演示在此处:https://dotnetfiddle.net/uUuy7v

1
感谢你的回答和工作演示!我认为我的使用“自动”一词是一个不好的选择。由于我正在反序列化各种各样的类,我希望有一个“通用”或不可知的解决方案,可以处理任何这种数组结构 [ "FOO", "..." ] 的发生,而无需为每个C#类型单独编码。在我的示例中,我只有 VetAnimal,但实际上有更多。我也不能保证检查属性(就像你在步骤#2中使用了 vet_id 一样)可以确定所引用对象的类型。 - Ben Jenkinson
1
我认为使用BAR字典的内容填充某些东西是正确的方法。我需要推迟每个引用值的反序列化,直到调用属性的JsonConverter并且已知属性类型。但据我所知,JsonConverter是由正在被反序列化/序列化的已知类型触发的,而不是JSON中数组结构["FOO", "..."]的存在。 - Ben Jenkinson
抱歉,已经过去一年了,我应该更早地接受你的答案作为最有帮助的答案! - Ben Jenkinson
@BenJenkinson 没问题。 - Brian Rogers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接