How to handle object references in JSON document with Newtonsoft Json.NET?

丶灬走出姿态 提交于 2021-01-28 11:39:47

问题


I have a json dataset that comes with standard data fields and reference fields. It looks something like this:

[
    {
        "id":1,
        "name":"Book",
        "description":"Something you can read"
    },
    {
        "id":2,
        "name":"newspaper",
        "description": {
            "ref":"0.description"
        }
    }
]

This is my data model:

public class PhysicalObject {
    [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Default)]
    public int id;

    [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default)]
    public string name;

    [Newtonsoft.Json.JsonProperty("description", Required = Newtonsoft.Json.Required.Default)]  // FIXED should have been description not desc
    public string desc;
}

Every property in the json file has a specific type such as int for id, and string for description however every property can also link to another via a ref. In this case, the description of id= 2 is the same as id = 1

Is there a way via Error handling or creating some kind of fallback deserialization that can be applied that can allow me to serialize the ref?

Note that because of other requirements, I must use the Newtonsoft Json.NET library to resolve this. Information about other libraries or techniques to solve this are informative but probably won't resolve the issue.


回答1:


You can preload your JSON into a JToken hierarchy, then use LINQ to JSON to replace objects of the form {"ref":"some.period-separated.path"} with the tokens indicated in the path. Then subsequently the JToken hierarchy can be deserialized to your final model.

The following extension method does the trick:

public static partial class JsonExtensions
{
    const string refPropertyName = "ref";

    public static void ResolveRefererences(JToken root)
    {
        if (!(root is JContainer container))
            return;
        var refs = container.Descendants().OfType<JObject>().Where(o => IsRefObject(o)).ToList();
        Console.WriteLine(JsonConvert.SerializeObject(refs));
        foreach (var refObj in refs)
        {
            var path = GetRefObjectValue(refObj);
            var original = ResolveRef(root, path);
            if (original != null)
                refObj.Replace(original);
        }
    }

    static bool IsRefObject(JObject obj)
    {
        return GetRefObjectValue(obj) != null;
    }

    static string GetRefObjectValue(JObject obj)
    {
        if (obj.Count == 1)
        {
            var refValue = obj[refPropertyName];
            if (refValue != null && refValue.Type == JTokenType.String)
            {
                return (string)refValue;
            }
        }
        return null;
    }

    static JToken ResolveRef(JToken token, string path)
    {
        // TODO: determine whether it is possible for a property name to contain a '.' character, and if so, how the path will look.
        var components = path.Split('.'); 

        foreach (var component in components)
        {
            if (token is JObject obj)
                token = obj[component];
            else if (token is JArray array)
                token = token[int.Parse(component, NumberFormatInfo.InvariantInfo)];
            else
                // Or maybe just return null?
                throw new JsonException("Unexpected token type.");
        }
        return token;
    }
} 

Then you would use it as follows:

// Load into intermediate JToken hierarchy; do not perform DateTime recognition yet.
var root = JsonConvert.DeserializeObject<JToken>(jsonString, new JsonSerializerSettings { DateParseHandling = DateParseHandling.None });

// Replace {"ref": "...") objects with their references.
JsonExtensions.ResolveRefererences(root);

// Deserialize directly to final model.  DateTime recognition should get performed now.
var list = root.ToObject<List<PhysicalObject>>();

Notes:

  1. This solution does not attempt to preserve references, i.e. to make the deserialized {"ref":"some.period-separated.path"} refer to the same instance as the deserialized original. While Json.NET does have functionality to preserve object references via "$ref" and "$id" properties, it has several limitations including:

    • It does not handle references to primitives, only objects and arrays.

    • It does not allow for forward-references, only backward references. It's not clear from the question whether the "ref" properties in your JSON might refer to values later in the document.


    These limitations would complicate transforming the reference syntax shown in the question into Json.NET's syntax.

  2. It's a good idea to defer DateTime recognition until final deserialization. If your model has string properties whose JSON values might happen to look like ISO 8601 dates then premature date recognition may cause the string values to get modified.

Demo fiddle here.




回答2:


You'll need some mechanism that resolves those references.

Here are two approaches:

1. Using built-in reference handling

One such mechanism is the Newtonsoft serializer's PreserveReferencesHandling property, which does exactly what you describe, except it looks for $id and $ref instead of id and ref.

To use this, you could transform the JSON tree before it's being converted into typed objects, by first reading it into a JSON tree representation (using JToken.Parse), then traversing this tree, replacing id and ref properties with $id and $ref (since the intermediate JSON tree is modifiable and dynamic in nature, you can do this easily).

Then you could convert this transformed tree into your typed objects using the built-in reference resolution mechanism, by using JObject.CreateReader to obtain a JsonReader over the transformed tree, which you can give to JsonSerializer.Deserialize<T> to instruct it to deserialize it into your desired type.

T DeserializeJsonWithReferences<T>(string input) 
{
  var jsonTree = JToken.Parse(jsonString);
  TransformJsonTree(jsonTree);  // renames `id` and `ref` properties in-place
  var jsonReader = jsonTree.CreateReader();
  var jsonSerializer = new JsonSerializer() { 
    PreserveReferencesHandling = PreserveReferenceHandling.All 
  };
  var deserialized = jsonSerializer.Deserialize<T>(jsonReader);
  return deserialized;
}

void TransformJsonTree(JToken token)
{
  var container = token as JContainer;
  if (container == null) 
    return;

  foreach (propName in SpecialPropertyNames)  // {"id", "ref"}
  {
    objects = container
      .Descendants()
      .OfType<JObject>()
      .Where(x => x.ContainsKey(propName));

    foreach (obj in objects) 
    {
      obj["$" + propName] = obj[propName];
      obj.Remove(propName);
    }
  }
}

2. Rolling your own reference resolution layer

A more complex approach, if you want to do it yourself: you'll need to add your own reference resolution layer, which would transform the JSON tree before it's converted into typed objects.

Here again, you can start by reading the JSON stream into a JSON tree representation. Then then you'll need to traverse that tree twice:

  • On the first traversal you'll look for objects with an id property, and record them in a dictionary (from id to the object containing it).

  • On the second traversal you'll look for objects with a ref property, and replace those ref-objects with the appropriate value, by looking up the referenced object by its id in the dictionary you created earlier, then navigating its properties according to the property chain described in the ref value. For example, if the ref is 3.address.city, you'll look up the object with ID 3, then find the value of its address property, and then the value of the city property of that value, and that would be the final value of the reference.

Once the JSON tree has been transformed and all reference-objects have been replaced by their corresponding referenced values, you can convert the JSON tree into a typed object.

Code-wise that would be exactly the same as the previous example, except in transformJsonTree, instead of just renaming the id and ref properties, you'll have to implement the actual lookup and reference resolution logic.

It could look something like this:

IDictionary<string, JToken> BuildIdMap(JContainer container)
{
  return container
    .Descendants()
    .OfType<JObject>()
    .Where(obj => obj.ContainsKey(IdPropertyName)
    .ToDictionary(obj => obj[IdPropertyName], obj => obj);
}

JToken LookupReferenceValue(string referenceString, IDictionary<string, JObject> idToObjectMap)
{
  var elements = referenceString.Split('.');
  var obj = idToObjectMap(elements[0]);

  for (int i = 1; i < elements.Length; i++) 
  {
    var elem = elements[i];
    switch (obj) 
    {
      case JArray jarr:
        obj = arr[elem];  // elem is a property name
        break;
      case JObject jobj:
        obj = jobj[int.Parse(elem)];  // elem is an array index
        break;
      default:
        throw Exception("You should throw a meaningful exception here"); 
    }
  }
}

void ResolveReferences(JContainer container, IDictionary<string, JObject> idToObjectMap)
{
  refObjects = container
    .Descendants()
    .OfType<JObject>()
    .Where(obj.Count == 1 && obj => obj.ContainsKey(RefPropertyName))

  foreach (var refObject in refObjects) 
  {
    referenceString = refObject[RefPropertyName];
    referencedValue = LookupReferenceValue(refObject, idToObjectMap)
    refObject.Replace(referencedValue);
  }
}

EDIT: Also take a look at JToken.SelectToken which allows you to navigate a property chain from a string or JsonPath, saving a lot of trouble from the above (assuming the reference syntax in your document matches the one Newtonsoft supports, e.g. with respect to array indices).

JToken LookupReferenceValue(string referenceString, IDictionary<string, JObject> idToObjectMap)
{
  var parts = referenceString.Split('.', 1); // only split on first '.'
  var id = parts[0];
  var tokenPath = parts[1];
  var referencedObject = idToObjectMap[id];
  var referencedValue = referencedObject.SelectToken(tokenPath);
  return referencedValue;
}

It's been years since I wrote any C# so please excuse any syntax errors or non-idiomatic usage. But that's the general idea.



来源:https://stackoverflow.com/questions/60117142/how-to-handle-object-references-in-json-document-with-newtonsoft-json-net

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!