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
JsonReaderException
is caused specifically by the following line:SerializableTreeItem loaded = JsonConvert.DeserializeObject<SerializableTreeItem>((string)reader.Value ?? string.Empty);
JsonReader.Value
is 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
SerializableTreeItem
to aJObject
, then write theJObject
. Just serializeSerializableTreeItem
directly and skip the intermediateJObject
representation.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
NavTree
from 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; } }