问题
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:
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.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 (fromid
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 itsid
in the dictionary you created earlier, then navigating its properties according to the property chain described in theref
value. For example, if the ref is3.address.city
, you'll look up the object with ID 3, then find the value of itsaddress
property, and then the value of thecity
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