-1

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.

user3190036
  • 463
  • 6
  • 15
  • It's unclear what you are trying to do here because you don't share a [mcve], but `JsonConvert.DeserializeObject((string)reader.Value ?? string.Empty);` is just wrong. `reader.Value` is merely the current token value, you can't deserialize an entire DOM subtree from it. You need to pass in the `JsonSerializer` and do `var loaded = serializer.Deserialize(reader);`. – dbc Dec 22 '22 at 16:28

1 Answers1

1

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 : Newtonsoft.Json.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:

  1. 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 your SerializableTreeItem. Instead, use JsonSerializer.Deserialize<T>(JsonReader) to deserialize the JSON subtree anchored by the current JSON token.

  2. When writing, there should be no need to serialize your SerializableTreeItem to a JObject, then write the JObject. Just serialize SerializableTreeItem directly and skip the intermediate JObject representation.

  3. 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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 1
    Thanks so much. As you noted, I misunderstood what reader.Value was for. I am used to dealing with blobs of json as strings. Thanks for taking the time to lay things out and give a clear answer. – user3190036 Dec 22 '22 at 18:30
  • Marking this as the answer was premature. The answer is for a whole different json library. I could convert my entire project to use System.Text.Json but i have had bad experiences in the past with it and I am more used to Json.Net – user3190036 Dec 23 '22 at 18:34
  • @user3190036 - *The answer is for a whole different json library.* -- this statement is not correct. The first `NavTreeConverter : JsonConverter` is indeed from Json.NET. It inherits from [`Newtonsoft.Json.JsonConverter`](https://www.newtonsoft.com/json/help/html/t_newtonsoft_json_jsonconverter_1.htm). I added the second NavTreeConverter as an example of how separating your DTO and converter would make it easier to work with different serializers. I am sorry to have confused you. I've edited the answer to clarify the namespaces of the base converters. – dbc Dec 23 '22 at 18:52
  • Thanks, I did not mean to criticise your answer, it was very helpful and I used your suggestions to tidy up my code. There was another important point that was causing my issue though, the properties of the DTO were read only and Json.Net could not write to them properly. – user3190036 Dec 26 '22 at 23:04
  • @user3190036 - your question does not show any read-only properties -- or any properties at all, for that matter. So, while [Why my dull Newtonsoft.Json deserialization code does not work?](https://stackoverflow.com/q/66972589/3744182) may have resolved your problem, it doesn't really address your question. – dbc Dec 28 '22 at 18:07
  • That is a fair comment. The reality is it is a bad question. What is the normal procedure in cases like this? Should I just delete it? Your answer still has value, it seems a pity to. On the other hand I could change the title, if it was just called 'how do I set up this JsonConverter correctly' then the question and answer could stand as they are. I apologise for the inconvenience here but these things happen. It is still not clear what the actual error message in the title means and what causes it, and it seems like it doesn't matter either. – user3190036 Dec 30 '22 at 11:43