Search code examples
c#jsonsystem.text.json

System.Text.Json Utf8JsonReader is reading the array values twice


I am trying to deserialize with the Utf8JsonReader i now that it's can be made easily but i need to make a custom deserialization. When i try to deserialize an array with object inside it's reading it twice, as shown in this screen capture:

Screen capture showing the file was read twice

I don't found many info how to use the Utf8JsonReader so maybe i am making all of this not right.

This is my deserialization code:

public Map? ReadYaml(string json)
{
    JsonReaderOptions options = new() { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip };
    Utf8JsonReader reader = new(System.Text.Encoding.UTF8.GetBytes(json), options);

    Map map = new();

    while (reader.Read())
    {
        switch (reader.TokenType)
        {
            case JsonTokenType.PropertyName:
                if (reader.ValueTextEquals("Map name"))
                {
                    reader.Read();

                    map = new(reader.GetString());
                }
                if (reader.ValueTextEquals("Entities"))
                {
                    reader.Read();

                    if (reader.TokenType == JsonTokenType.StartArray)
                    {
                        while (reader.Read())
                        {
                            if (reader.TokenType == JsonTokenType.EndArray)
                                break;

                            if (reader.TokenType == JsonTokenType.StartObject)
                            {
                                while (reader.Read())
                                {
                                    if (reader.TokenType == JsonTokenType.EndObject)
                                        break;

                                    if (reader.TokenType == JsonTokenType.PropertyName)
                                    {
                                        reader.Read();
                                        if (reader.TokenType == JsonTokenType.String)
                                        {
                                            var str = reader.GetString();
                                            if (str != null)
                                            {
                                                Debug.Log(str);
                                            }
                                        }
                                    }
                                }
                            }
                            else
                            {
                                break;
                            }
                        }
                    }
                }
                break;
        }
    }
    return map;
}

This is my JSON file to deserialize:


{
  "Map name": "NewMap",
  "Entities": [
    {
      "Name": "Entity",
      "Active": "True",
      "GUID": "f06ed7d3-f1b3-4cfd-a623-a7ceb17dbced"
    },
    {
      "Name": "Entity #1",
      "Active": "True",
      "GUID": "03788f46-e430-4882-a097-e860d2c7aed5"
    }
  ],
  "Entity components": [
    {
      "Component type": "ExtremeEngine.Transform",
      "Component data": [
        {
          "Position": null,
          "Rotation": null,
          "Scale": null,
          "Entity": "f06ed7d3-f1b3-4cfd-a623-a7ceb17dbced",
          "GUIHeaderIsOpened": "True",
          "Enabled": "True",
          "GUID": "95dea154-6f00-41b0-bdec-fe78cca589c3"
        }
      ]
    },
    {
      "Component type": "ExtremeEngine.Transform",
      "Component data": [
        {
          "Position": null,
          "Rotation": null,
          "Scale": null,
          "Entity": "03788f46-e430-4882-a097-e860d2c7aed5",
          "GUIHeaderIsOpened": "True",
          "Enabled": "True",
          "GUID": "d92b131a-a82f-487f-b1c1-25ff13496284"
        }
      ]
    }
  ]
}

Solution

  • Utf8JsonReader is very difficult to work with directly because it is designed to be used for asynchronous reading of JSON token-by-token from a sequence of UTF8 byte spans, typically generated from a pipeline.

    Rather than working directly with the reader, it will be much easier to deserialize your JSON to some Data Transfer Object you have designed that corresponds to the JSON. After it has been deserialized, you can map it to your final model using LINQ, AutoMapper, or any other tool you prefer.

    Using https://json2csharp.com/ which is one of the tools mentioned in How to auto-generate a C# class file from a JSON string, I generated the following data model:

    public class MapDTO
    {
        // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/immutability
        // Important: System.Text.Json requires that the constructor parameter names are the same as the corresponding property names, ignoring differences in case.
        [JsonConstructor]
        public MapDTO(string mapname) => this.Mapname = mapname;
        public MapDTO() : this("") { }
        
        [JsonPropertyName("Map name")]
        public string Mapname { get; }
    
        [JsonPropertyName("Entities")]
        public List<EntityDTO> Entities { get; set; }
    
        [JsonPropertyName("Entity components")]
        public List<EntityComponentDTO> Entitycomponents { get; set; }
    }
    
    public class ComponentDatumDTO
    {
        [JsonPropertyName("Position")]
        public object Position { get; set; } // TODO: update with the correct type
    
        [JsonPropertyName("Rotation")]
        public object Rotation { get; set; } // TODO: update with the correct type
    
        [JsonPropertyName("Scale")]
        public object Scale { get; set; } // TODO: update with the correct type
    
        [JsonPropertyName("Entity")]
        public string Entity { get; set; }
    
        [JsonPropertyName("GUIHeaderIsOpened")]
        public string GUIHeaderIsOpened { get; set; }
    
        [JsonPropertyName("Enabled")]
        public string Enabled { get; set; }
    
        [JsonPropertyName("GUID")]
        public Guid GUID { get; set; } // Changed from string to Guid
    }
    
    public class EntityDTO
    {
        [JsonPropertyName("Name")]
        public string Name { get; set; }
    
        [JsonPropertyName("Active")]
        public string Active { get; set; }
    
        [JsonPropertyName("GUID")]
        public Guid GUID { get; set; } // Changed from string to Guid
    }
    
    public class EntityComponentDTO
    {
        [JsonPropertyName("Component type")]
        public string Componenttype { get; set; }
    
        [JsonPropertyName("Component data")]
        public List<ComponentDatumDTO> Componentdata { get; set; }
    }
    

    After generating the model, I made the following smallish changes:

    • I introduced a parameterized constructor for Mapname.

    • I changed type of the GUID properties from string to Guid.

    • I appended DTO to every type name

    With that model, your JSON can be deserialized from a string or a file to a DTO as follows:

    static JsonSerializerOptions DefaultOptions { get; } = new() { AllowTrailingCommas = true, ReadCommentHandling = JsonCommentHandling.Skip };
    
    public MapDTO? ReadYamlFromFile(string filePath)
    {
        using (var stream = File.OpenRead(filePath))
            return JsonSerializer.Deserialize<MapDTO>(stream, DefaultOptions);
    }
    
    public MapDTO? ReadYaml(string json) =>
        JsonSerializer.Deserialize<MapDTO>(json, DefaultOptions);
    

    Then afterwards you can map that to your final model without having to worry about JSON, or about Utf8JsonReader.

    See also:

    Demo fiddle here.