不破坏“$ ref”引用的JSON树修改

时间:2014-05-06 22:10:01

标签: c# json json.net

问题与JSON有关,JSON是 PreserveReferencesHandling 设置为 PreserveReferencesHandling.Objects 的序列化结果。我正在寻找修改JSON树分支的巧妙方法(特别是删除或替换分支),以便不破坏引用处理。

考虑遵循JSON:

{
  "OuterGroup": {
    "ElementA": {
      "$id": "1",
      "data": "A"
    },
    "ElementB": {
      "$id": "2",
      "data": "B"
    }
  },
  "OuterElement": {
    "$ref": "1"
  }
}

我需要更换或删除ElementA。如果我通过 JToken.Replace 执行此操作,则外部元素引用将被破坏。

我想到的第一个解决方案就像在遍历任何修改之前遍历树并用引用的部分替换引用一样简单。我正在寻找更优雅的方法。

问题背景:

我使用的系统中的一些数据是以JSON格式保存的。出于某种原因,我必须在不反序列化的情况下进行迁移(旧模型不可用)。

1 个答案:

答案 0 :(得分:1)

尝试直接修改JSON会产生问题,特别是如果您的JSON深度嵌套或者对同一个对象有多个引用,那么我就不会尝试这种方法。

我认为最好的方法是将JSON实际反序列化为对象层次结构(当然保留引用),然后根据需要修改对象,最后使用PreserveReferencesHandling.Objects将层次结构序列化为JSON设置。

如果您拥有原始对象模型,这将非常简单,因为Json.Net已经支持此功能。但是,由于您没有原始对象模型,因此您必须将JSON反序列化为通用的。通常我会说使用JTokens对此非常合适,但事实证明JToken不支持保留对象引用。

因此,看起来唯一真正的选择是手动处理反序列化。幸运的是,这并不像听起来那么糟糕。您可以使用JsonTextReader来读取JSON,同时递归地构建通用字典和列表的层次结构来保存数据。在此过程中,您可以使用另一个字典作为查找表来跟踪对象引用。

下面是一个封装此逻辑的方法。注意这种方法确实做了一些假设:

  • 您的JSON格式正确(当然)
  • 对于$id的JSON对象,$id是对象中的第一个属性,表示相对于JSON中所有其他对象的唯一ID
  • 对于$ref的JSON对象,$ref是该对象中唯一的属性,它引用的是先前在JSON中出现的$id

如果使用Json.Net首先使用PreserveReferencesHandling.Objects设置创建JSON,则所有这些都应该成立。

public static object DeserializePreservingReferences(string json)
{
    using (JsonTextReader reader = new JsonTextReader(new StringReader(json)))
    {
        return DeserializePreservingReferences(reader, 
                   new Dictionary<string, Dictionary<string, object>>());
    }
}

private static object DeserializePreservingReferences(JsonTextReader reader,
                         Dictionary<string, Dictionary<string, object>> lookup)
{
    if (reader.TokenType == JsonToken.None)
    {
        reader.Read();
    }

    if (reader.TokenType == JsonToken.StartArray)
    {
        List<object> list = new List<object>();
        while (reader.Read() && reader.TokenType != JsonToken.EndArray)
        {
            list.Add(DeserializePreservingReferences(reader, lookup));
        }
        return list;
    }

    if (reader.TokenType == JsonToken.StartObject)
    {
        Dictionary<string, object> dict = new Dictionary<string, object>();
        while (reader.Read() && reader.TokenType != JsonToken.EndObject)
        {
            string propName = (string)reader.Value;
            reader.Read();

            if (propName == "$ref")
            {
                dict = lookup[reader.Value.ToString()];
            }
            else if (propName == "$id")
            {
                lookup[reader.Value.ToString()] = dict;
            }
            else
            {
                dict.Add(propName, DeserializePreservingReferences(reader, lookup));
            }
        }
        return dict;
    }

    return new JValue(reader.Value).Value;
}

使用此方法,您可以完成您最初想要的任务。这是一个简短的演示:

class Program
{
    static void Main(string[] args)
    {
        string json = @"
        {
            ""OuterGroup"": {
                ""ElementA"": {
                    ""$id"": ""1"",
                    ""data"": ""A""
                },
                ""ElementB"": {
                    ""$id"": ""2"",
                    ""data"": ""B""
                }
            },
            ""OuterElement"": {
                ""$ref"": ""1""
            }
        }";

        var root = (Dictionary<string, object>)DeserializePreservingReferences(json);
        var g = (Dictionary<string, object>)root["OuterGroup"];
        g.Remove("ElementA");

        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.PreserveReferencesHandling = PreserveReferencesHandling.Objects;
        settings.Formatting = Formatting.Indented;
        Console.WriteLine(JsonConvert.SerializeObject(root, settings));
    }
}

以下是演示程序的输出。尽管引用在新JSON中可能具有不同的$id值,但仍保留了引用。请注意,已移除的data中的ElementA已根据需要移至OuterElement

{
  "$id": "1",
  "OuterGroup": {
    "$id": "2",
    "ElementB": {
      "$id": "3",
      "data": "B"
    }
  },
  "OuterElement": {
    "$id": "4",
    "data": "A"
  }
}