Reputation: 762
I have a big data model for cosmosdb which was originally designed for ef code first. That means it have a lot of bidirectional references. Simplified json for this doc looks like this -
Root : {
Prop1 : {
X : {
"$id" : 1 ,
...
X: { "$ref": 1 }
Y: {
"$id" : 2
X: { "$ref": 1 }
}
}
},
Prop2 : {
Y: { "$ref" : 2, ... }
}
}
If I'll put jsonIgnore attribute for Prop1.X, then somewhere in Prop2.Y I'll got missing fields. Have no idea how to correctly remove (or put jsonIgnore) X field.
Below is my deserializer settings:
public static readonly JsonSerializerSettings CosmosSettings = new JsonSerializerSettings
{
DateParseHandling = DateParseHandling.None,
Converters =
{
new StringEnumConverter(),
},
MissingMemberHandling = MissingMemberHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore,
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
}
var client = new DocumentClient(uri, accountKey, CosmosSettings, connectionPolicy);
Upvotes: 0
Views: 1065
Reputation: 116805
Your basic problem is that, by removing Prop1
, you prevented Json.NET from deserializing some objects when they are first encountered in the JSON. Later, when references to them are encountered, the references cannot be resolved because no object was deserialized and saved for reuse. The solution is to restore the obsoleted property as a set-only, private property that does nothing so as to trigger the necessary deserialization. Details as follows.
Your problem can be reproduced more simply with the following models (demo fiddle #1 here):
public class RootObjectOld
{
public InnerObject Prop1 { get; set; }
public InnerObject Prop2 { get; set; }
}
public class RootObjectNew
{
// A new version of RootObjectOld from which Prop1 was removed
public InnerObject Prop2 { get; set; }
}
public class InnerObject
{
public string Value { get; set; }
}
And the following code:
var inner = new InnerObject { Value = "inner value" };
var oldroot = new RootObjectOld { Prop1 = inner, Prop2 = inner };
var json = JsonConvert.SerializeObject(oldroot, Formatting.Indented, CosmosSettings);
var newrootback = JsonConvert.DeserializeObject<RootObjectNew>(json, CosmosSettings);
Assert.IsNotNull(newrootback.Prop2, "newrootback.Prop2"); // FAILS!
Assert.AreEqual(inner.Value, newrootback.Prop2.Value);
The reason for the failure is as follows. The JSON created for oldroot
looks like:
{
"$id": "1",
"Prop1": {
"$id": "2",
"Value": "inner value"
},
"Prop2": {
"$ref": "2"
}
}
As you can see, the InnerObject
is defined when it is first encountered in the JSON document, and written as a reference on all subsequent occurrences. Then, when deserializing this JSON, Json.NET will deserialize the $id
object to the type currently being deserialized when it is encountered, then cache the result for reuse when $ref
objects are subsequently encountered. But by obsoleting Prop1
you obsoleted the member that triggers the initial deserialization. Thus no deserialization occurs, no value is cached, and subsequent references cannot be resolved.
(Now, theoretically Json.NET could handle this situation by caching unreferenced $id
objects in some table somewhere as a JToken
, then deserialize to the appropriate target type when a reference is encountered. However, this is not implemented. Rewinding the JSON stream to find the original $id
definition is also not implemented because Json.NET is a single-pass serializer.)
To resolve the problem we somehow need to trigger an appropriate deserialization when the definition for InnerObject
is encountered. The easiest way to do this is with some private, set-only property that does nothing. The property can be private as long as it is marked with [JsonProperty]
:
public class RootObjectNew
{
[JsonProperty] private InnerObject Prop1 { set { } }
public InnerObject Prop2 { get; set; }
}
Because the setter does nothing, you're not actually saving the value of Prop1
anywhere, so the member was indeed obsoleted, leaving only a "stub" setter for compatibility.
Demo fiddle #2 here.
Upvotes: 1