Home > Blockchain >  JsonReaderException Unexpected state: ObjectStart
JsonReaderException Unexpected state: ObjectStart

Time:12-23

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:

  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. 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;
        }
    }
    
  • Related