【问题标题】:Controlling JSON .NET's reference ID generation控制 JSON .NET 的参考 ID 生成
【发布时间】:2020-03-29 11:47:03
【问题描述】:

我希望能够控制 JSON .NET 如何生成其元引用 ID,例如 "$id": "1"。取以下代码:

public class Person
{
    public string Name { get; set; }

    public Person Mother { get; set; }
}

.

var settings = new Newtonsoft.Json.JsonSerializerSettings
{
    PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects,
    Formatting = Newtonsoft.Json.Formatting.Indented
};

Newtonsoft.Json.JsonConvert.DefaultSettings = () => settings;

var person = new Person
{
    Name = "bob",
    Mother = new Person { Name = "jane" }
};
var personJson = JsonConvert.SerializeObject(person);
var motherJson = JsonConvert.SerializeObject(person.Mother);

person 的 JSON 如下所示:

{
  "$id": "1",
  "Name": "bob",
  "Mother": {
    "$id": "2",
    "Name": "jane",
    "Mother": null
  }
}

但是,如果我直接序列化person.Mother,JSON 看起来像这样:

{
  "$id": "1",
  "Name": "jane",
  "Mother": null
}

在第一个 JSON 中,Jane 是 "$id": "2",但直接序列化 Jane 是 "$id": "1"。这是我在正常情况下所期望的行为,因为序列化程序按照遍历对象的顺序分配 ID,但我真的很想覆盖 ID 生成,以便我可以将其设为对象引用本身的哈希。这样一来,无论是作为父级成员序列化还是单独序列化,Jane 每次都会为每个运行的程序实例生成相同的 ID。

更新

对于所选答案中的示例代码和评论中的建议,我使用了IReferenceResolver。事实证明我不能使用它,但无论如何我都会包含下面的代码。这不起作用的原因是因为我试图将 JSON.NET 混为一个快速而肮脏的克隆工具,所以我不能因为它不适合我的需要而责备它。从那以后,我开始使用自己的自定义克隆实用程序,因此不再需要它。

public class ObjectReferenceResolver : Newtonsoft.Json.Serialization.IReferenceResolver
{
    readonly Dictionary<object, int> objectDic = new Dictionary<object, int>();
    int maxId = 0;

    //Called during serialization
    public string GetReference(object context, object value)
    {
        //This method will return the meta $id that you choose. In this example, I am storing
        //object references in a dictionary with an incremented ID. If the reference exists, I
        //return its ID. Otherwise, I increment the ID and add the reference to the dictionary.

        var id = 0;

        if (objectDic.ContainsKey(value))
        {
            id = objectDic[value];
        }
        else
        {
            objectDic[value] = maxId++;
        }

        return id.ToString();
    }

    //Called during serialization
    public bool IsReferenced(object context, object value)
    {
        //Return whether or not value exists in a reference bank.
        //If false, the JSON will return as a full JSON object with "$id": "x"
        //If true, the JSON will return "$ref": "x"
        return objectDic.ContainsKey(value);
    }

    //Called during deserialization
    public void AddReference(object context, string reference, object value)
    {
        //This method is called after the deserializer has created a new instance of the
        //object. At this time, it's only the initial instance and no properties have been set.
        //This method presents a problem because it does not allow you to create the instance or
        //retrieve it from a repo and then return it for later use by the reference resolver.
        //Therefore, I have to find the existing object by $id, remove it, and then add the new 
        //object created by the deseralizer. This creates the possibility for two instances of
        //the same data object to exist within the running application, so, unfortunately, this
        //will not work.

        var e = objectDic.First(x => x.Value.ToString() == reference).Key;

        objectDic.Remove(e);

        objectDic[value] = reference.ParseInt().Value;
    }

    //Called during deserialization
    public object ResolveReference(object context, string reference)
    {
        //This method retrieves an existing reference by $id and returns it.

        var value = objectDic.FirstOrDefault(x => x.Value.ToString() == reference).Key;

        return value;
    }
}

【问题讨论】:

  • @BrianRogers 请注意,我说的是“对象引用本身”。没有两个对象可以共享同一个引用。
  • 自定义IReferenceResolver 可能会满足您的需求。请参阅:How to use custom reference resolving with JSON.NET
  • 你看How to use custom reference resolving with JSON.NET了吗?如果是这样,您的问题是 1)如何为您的对象生成唯一哈希? 2)如何将唯一哈希挂钩到IReferenceResolver
  • 很奇怪,我没有收到您之前评论的通知。不过,我看了看,这个问题似乎是在询问如何将 JSON 中的 categoryID 映射到导航属性的 id 属性,这与我所问的完全不同。我指的是 $id 属性,它只存在于反序列化器用来保留引用的 JSON 中。我想覆盖序列化程序返回的值,以便它显示类似"$id": "whateveriwant" 而不是"$id": "1"
  • accepted answer 实际上展示了如何使用 Guid 作为 $id$ref 的值。

标签: c# .net serialization reference json.net


【解决方案1】:

根据其他人的建议,您需要自定义IReferenceResolver

class PersonNameAsIdResolver : IReferenceResolver
{
    public void AddReference(object context, string reference, object value)
    {
        // This method is called during deserialize for $id
    }

    public string GetReference(object context, object value)
    {
        // Returns person name as value of $id
        return ((Person)value).Name;
    }

    public bool IsReferenced(object context, object value)
    {
        // Returns false, so that $id is used, not $ref.
        return false;
    }

    public object ResolveReference(object context, string reference)
    {
        // This method is called during deserialize for $ref
        return null;
    }
}

要使用它:

var settings = new Newtonsoft.Json.JsonSerializerSettings
{
    PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects,
    Formatting = Newtonsoft.Json.Formatting.Indented
};

settings.ReferenceResolverProvider = ()=> new PersonNameAsIdResolver();

更新

回答 OP 的更新

AddReference 在填充对象时被调用,因此替换对象已经为时已晚。为了能够找到并填充 desired 对象,您需要一个 JsonConverter,它在引用解析器之前调用:

class PersonJsonConverter : JsonConverter
{
    private readonly PersonNameAsIdResolver _idResolver;

    public PersonJsonConverter(PersonNameAsIdResolver idResolver)
    {
        _idResolver = idResolver;
    }

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

    // Can't write. There's nothing to changing for writing scenario.
    public override bool CanWrite => false;

    public override object ReadJson(JsonReader reader, Type objectType, 
        object existingValue, JsonSerializer serializer)
    {
        var token = JToken.ReadFrom(reader);
        if (token.Type == JTokenType.Null)
        {
            return null;
        }

        var obj = (JObject)token;

        // The code below calls the resolver to find the existing instance.
        // This can stop JSON.NET creating a new instance.
        Person instance = null;
        var @id = obj["$id"].Value<string>();
        if (@id != null)
        {
            instance = (Person)_idResolver.ResolveReference(this, @id);
        }
        else
        {
            var @ref = obj["$ref"]?.Value<string>();
            if (@ref != null)
            {
                instance = (Person)_idResolver.ResolveReference(this, @ref);
            }
        }

        // Assuming can't resolve, create a new instance.
        if (instance == null)
        {
            instance = new Person();
        }

        // This will populate existing Person object if found
        serializer.Populate(obj.CreateReader(), instance);

        return instance;
    }

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

默认的序列化设置应该是这样的:

var settings = new Newtonsoft.Json.JsonSerializerSettings
{
    PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects,
    Formatting = Newtonsoft.Json.Formatting.Indented
};

var idResolver = new PersonNameAsIdResolver();
settings.Converters.Add(new PersonJsonConverter(idResolver));
settings.ReferenceResolverProvider = () => idResolver;

【讨论】:

  • 这就是我要找的。我将您标记为答案,并会尽快奖励赏金。不幸的是,这个解决方案对我不起作用。我将发布我的代码作为问题的答案,并解释原因(我基本上是在尝试将 JSON.NET 混为一种快速而肮脏的克隆方法,所以我不能因为它不能按我喜欢的方式工作而责怪它) .
  • @oscilatingcretin 查看更新。在您的情况下,您需要先解析 JsonConverter 中的实例,然后内部转换器会自动为您创建一个。
【解决方案2】:

可能的解决方案如下:

  1. 将 PreserveReferncesHandling 从 Objects 替换为 None:
    PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.None
  1. 在 Person 类中添加 Id 属性:
    public class Person
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public Person Mother { get; set; }
    }

完整的解决方案如下:

using System;
using Newtonsoft.Json;

namespace ControlJsonId
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("INICIO");
            var settings = new Newtonsoft.Json.JsonSerializerSettings
            {
                PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.None,
                Formatting = Newtonsoft.Json.Formatting.Indented,
            };

            Newtonsoft.Json.JsonConvert.DefaultSettings = () => settings;

            var person = new Person
            {
                Id = Guid.NewGuid().ToString(),
                Name = "bob",
                Mother = new Person { Id = string.Empty, Name = "jane" }
            };

            var personJson = JsonConvert.SerializeObject(person);
            Console.WriteLine(personJson);

            var motherJson = JsonConvert.SerializeObject(person.Mother);
            Console.WriteLine(motherJson);

            Console.WriteLine("FIN");
            Console.ReadKey();
        }
    }

    public class Person
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public Person Mother { get; set; }
    }
}

结果是:

INICIO
{
  "Id": "76d6b5f0-2be8-4d1d-aafe-fe1b4b7d6ae1",
  "Name": "bob",
  "Mother": {
    "Id": "",
    "Name": "jane",
    "Mother": null
  }
}
{
  "Id": "",
  "Name": "jane",
  "Mother": null
}
FIN

【讨论】:

  • 为了更好的解释。这个链接解释得很好:newtonsoft.com/json/help/html/ConditionalProperties.htm
  • 为什么要在类上放一个ID属性,又为什么随意给它分配GUID?我不是在谈论课堂上的物理ID。它是 JSON 序列化程序添加到 JSON 的元 ID,以便它知道在反序列化时如何保留引用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-05-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-18
  • 1970-01-01
相关资源
最近更新 更多