I am trying to serialize a tree but I only need a tiny part of the data of the object (its a UI tree), so I wrote a custom converter.
The converter simply passes the reader and writer to the object
public override void WriteJson(JsonWriter writer, NavTree value, JsonSerializer serializer)
{
value.SaveAsJson(writer);
}
public override NavTree ReadJson(JsonReader reader, Type objectType, NavTree existingValue, bool hasExistingValue,
JsonSerializer serializer)
{
NavTree tree = hasExistingValue ? existingValue : new NavTree();
tree.LoadFromJson(reader);
return tree;
}
Serialization looks like this
public void SaveAsJson(JsonWriter writer)
{
SerializableTreeItem root = new (this.GetRoot());
JObject.FromObject(root).WriteTo(writer);
}
The object appears to serialize yielding json that looks something like
"NavTree": {
"Id": "All",
"IsCategory": true,
"Children": [
{
"Id": "https://popularresistance.org/feed/",
"IsCategory": false,
"Children": []
},
{
"Id": "https://www.aljazeera.com/xml/rss/all.xml",
"IsCategory": false,
"Children": []
},
... more children
The deserialization looks like:
public void LoadFromJson(JsonReader reader)
{
SerializableTreeItem loaded =
JsonConvert.DeserializeObject<SerializableTreeItem>((string)reader.Value ?? string.Empty);
if (loaded == null) return;
if (this.GetRoot() != null)
{
this.GetRoot().Free();
TreeItem root = this.CreateItem();
root.SetMetadata(0, RootMetaDataId);
}
this.AddItem(loaded, this.GetRoot());
}
Trying to access reader.Value at the start of the function returns null. Trying to access reader.ReadAsString() at the start results in:
Newtonsoft.Json.JsonReaderException: Unexpected state: ObjectStart. Path 'NavTree', line 669, position 14.
at Newtonsoft.Json.JsonTextReader.ReadStringValue(ReadType readType)
at Newtonsoft.Json.JsonTextReader.ReadAsString()
at Porifera.NavTree.LoadFromJson(JsonReader reader)
Line 669 is the first line of the json posted above. I never made a custom converter before so clearly I messed it up. The question is what did I do wrong? The json looks ok to me and all I really need is for the reader to deliver something and I can reconstruct the object.
CodePudding user response:
You are using SerializableTreeItem as a data transfer object for NavTree:
In the field of programming a data transfer object (DTO) is an object that carries data between processes.
What you should do is to refactor your code to separate the responsibilities for converting from JSON to your DTO, and from your DTO to your NavTree.
First, modify NavTree to remove all references to JsonReader or any other JSON types:
public partial class NavTree
{
public void PopulateFromSerializableTreeItem(SerializableTreeItem loaded)
{
if (loaded == null)
return;
if (this.GetRoot() != null)
{
this.GetRoot().Free();
TreeItem root = this.CreateItem();
root.SetMetadata(0, RootMetaDataId);
}
this.AddItem(loaded, this.GetRoot());
}
public SerializableTreeItem ToSerializableTreeItem()
=> new (this.GetRoot());
}
Now, rewrite your JsonConverter<NavTree> as follows:
public class NavTreeConverter : JsonConverter<NavTree>
{
public override void WriteJson(JsonWriter writer, NavTree value, JsonSerializer serializer) =>
serializer.Serialize(writer, value.ToSerializableTreeItem());
public override NavTree ReadJson(JsonReader reader, Type objectType, NavTree existingValue, bool hasExistingValue,
JsonSerializer serializer)
{
var loaded = serializer.Deserialize<SerializableTreeItem>(reader);
// Check for null and return null? Throw an exception?
var tree = hasExistingValue ? existingValue : new NavTree();
tree.PopulateFromSerializableTreeItem(loaded);
return tree;
}
}
And you should be good to go.
Notes:
Your
JsonReaderExceptionis caused specifically by the following line:SerializableTreeItem loaded = JsonConvert.DeserializeObject<SerializableTreeItem>((string)reader.Value ?? string.Empty);JsonReader.Valueis the value of the current JSON token, but you are using it as if it contained the entire JSON subtree corresponding to yourSerializableTreeItem. Instead, useJsonSerializer.Deserialize<T>(JsonReader)to deserialize the JSON subtree anchored by the current JSON token.When writing, there should be no need to serialize your
SerializableTreeItemto aJObject, then write theJObject. Just serializeSerializableTreeItemdirectly and skip the intermediateJObjectrepresentation.By separating JSON serialization from DTO conversion, you will be able to more easily port your serialization code to System.Text.Json or any other serializer, if you eventually chose to do so. E.g. a converter for your
NavTreefrom System.Text.Json would look like:public class NavTreeConverter : System.Text.Json.Serialization.JsonConverter<NavTree> { public override void Write(Utf8JsonWriter writer, NavTree value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value.ToSerializableTreeItem(), options); public override NavTree Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var loaded = JsonSerializer.Deserialize<SerializableTreeItem>(ref reader, options); var tree = new NavTree(); // System.Text.Json does not have the ability to populate an exising value! tree.PopulateFromSerializableTreeItem(loaded); return tree; } }
