首页 > 解决方案 > 控制 JSON .NET 的参考 ID 生成

问题描述

我希望能够控制 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);

JSON forperson如下所示:

{
  "$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;
    }
}

标签: c#.netserializationreferencejson.net

解决方案


根据其他人的建议,您需要一个自定义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在填充对象时调用,因此替换对象为时已晚。为了能够找到并填充所需的对象,您需要一个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;

推荐阅读