3
\$\begingroup\$

Path of Exile is a PC game where players can list their items for sale. The game has a public API that serves JSON which contains all of these items. My application consumes that JSON and indexes the items in a way that is easily searchable.

Json.NET provides an easy, one-liner method of deserializing JSON to a C# model:

var root = JsonConvert.DeserializeObject<RootObject>(json);

It was not as fast as I needed it to be. I replaced it with a deserializer that is fast enough, and that code is what I would like to have reviewed.

Primary Concern

All feedback is welcome, but what drove me to post is how repetitive the code is. Utf8JsonReader and ReadOnlySpan are ref structs, which means neither can be used as a type arguments. I could not figure out how to write DRY code with that restriction.

For example, the implementations of methods ParseStash(), ParseItem(), ParseSocket(), and ParseProperty() are nearly identical.

Code

If you prefer downloading and running it, here is a minimal project on GitHub: https://github.com/d2erusher/JsonParserCodeReview

public class FastJsonParserService : IJsonParserService
{
    public RootObject Parse(string json)
    {
        ReadOnlySpan<byte> jsonUtf8 = Encoding.UTF8.GetBytes(json);
        var reader = new Utf8JsonReader(jsonUtf8, true, default);
        return ParseRootObject(ref reader);
    }

    private static RootObject ParseRootObject(ref Utf8JsonReader reader)
    {
        var root = new RootObject();

        reader.Read();
        while (reader.Read() 
               && reader.TokenType != JsonTokenType.EndObject)
        {
            var rootPropertyName = reader.ValueSpan;
            reader.Read();
            ParseRootObjectProperty(ref reader, rootPropertyName, root);
        }
        return root;
    }

    private static void ParseRootObjectProperty(ref Utf8JsonReader reader, ReadOnlySpan<byte> propertyName, RootObject root)
    {
        if (propertyName.SequenceEqual(PropertyNameBytes.BytesNextChangeId))
        {
            root.NextChangeId = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesStashes))
        {
            root.Stashes = ParseStashArray(ref reader);
        }
        else
        {
            Skip(ref reader);
        }
    }

    private static List<Stash> ParseStashArray(ref Utf8JsonReader reader)
    {
        var stashes = new List<Stash>();
        while (reader.Read() 
               && reader.TokenType != JsonTokenType.EndArray)
        {
            stashes.Add(ParseStash(ref reader));
        }

        return stashes;
    }

    private static Stash ParseStash(ref Utf8JsonReader reader)
    {
        var stash = new Stash();
        while (reader.Read() 
               && reader.TokenType != JsonTokenType.EndObject)
        {
            var stashPropertyName = reader.ValueSpan;
            ParseStashProperty(ref reader, stashPropertyName, stash);
        }
        return stash;
    }

    private static void ParseStashProperty(ref Utf8JsonReader reader, ReadOnlySpan<byte> propertyName, Stash stash)
    {
        if (propertyName.SequenceEqual(PropertyNameBytes.BytesAccountName))
        {
            reader.Read();
            if (reader.TokenType == JsonTokenType.String)
                stash.AccountName = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesLastCharacterName))
        {
            reader.Read();
            if (reader.TokenType == JsonTokenType.String)
                stash.LastCharacterName = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesId))
        {
            reader.Read();
            stash.Id = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesStash))
        {
            reader.Read();
            if (reader.TokenType == JsonTokenType.String)
                stash.Name = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesStashType))
        {
            reader.Read();
            stash.StashType = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesItems))
        {
            stash.Items = ParseItemArray(ref reader);
        }
        else
        {
            Skip(ref reader);
        }
    }

    private static List<Item> ParseItemArray(ref Utf8JsonReader reader)
    {
        reader.Read();
        var results = new List<Item>();
        while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
        {
            results.Add(ParseItem(ref reader));
        }
        return results;
    }

    private static Item ParseItem(ref Utf8JsonReader reader)
    {
        var item = new Item();
        while (reader.Read()
               && reader.TokenType != JsonTokenType.EndObject)
        {
            var itemPropertyName = reader.ValueSpan;
            ParseItemProperty(ref reader, itemPropertyName, item);
        }
        return item;
    }

    private static void ParseItemProperty(ref Utf8JsonReader reader, ReadOnlySpan<byte> propertyName, Item item)
    {
        if (propertyName.SequenceEqual(PropertyNameBytes.BytesVerified))
        {
            reader.Read();
            item.Verified = reader.GetBoolean();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesW))
        {
            reader.Read();
            item.W = reader.GetInt32();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesH))
        {
            reader.Read();
            item.H = reader.GetInt32();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesIlvl))
        {
            reader.Read();
            item.Ilvl = reader.GetInt32();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesTalismanTier))
        {
            reader.Read();
            item.TalismanTier = reader.GetInt32();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesCorrupted))
        {
            reader.Read();
            item.Corrupted = reader.GetBoolean();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesDuplicated))
        {
            reader.Read();
            item.Duplicated = reader.GetBoolean();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesStackSize))
        {
            reader.Read();
            item.StackSize = reader.GetInt32();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesMaxStackSize))
        {
            reader.Read();
            item.MaxStackSize = reader.GetInt32();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesIcon))
        {
            reader.Read();
            item.Icon = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesArtFilename))
        {
            reader.Read();
            item.ArtFilename = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesLeague))
        {
            reader.Read();
            item.League = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesId))
        {
            reader.Read();
            item.Id = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesSockets))
        {
            item.Sockets = ParseSocketArray(ref reader);
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesName))
        {
            reader.Read();
            item.Name = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesSecDescrText))
        {
            reader.Read();
            item.SecDescrText = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesDescrText))
        {
            reader.Read();
            item.DescrText = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesIdentified))
        {
            reader.Read();
            item.Identified = reader.GetBoolean();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesNote))
        {
            reader.Read();
            item.Note = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesProperties))
        {
            item.Properties = ParsePropertyArray(ref reader);
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesAdditionalProperties))
        {
            item.AdditionalProperties = ParsePropertyArray(ref reader);
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesRequirements))
        {
            item.Requirements = ParsePropertyArray(ref reader);
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesNextLevelRequirements))
        {
            item.NextLevelRequirements = ParsePropertyArray(ref reader);
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesExplicitMods))
        {
            reader.Read();
            item.ExplicitMods = ParseStringArray(ref reader);
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesImplicitMods))
        {
            reader.Read();
            item.ImplicitMods = ParseStringArray(ref reader);
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesUtilityMods))
        {
            reader.Read();
            item.UtilityMods = ParseStringArray(ref reader);
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesCraftedMods))
        {
            reader.Read();
            item.CraftedMods = ParseStringArray(ref reader);
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesEnchantMods))
        {
            reader.Read();
            item.EnchantMods = ParseStringArray(ref reader);
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesFlavourText))
        {
            reader.Read();
            item.FlavourText = ParseStringArray(ref reader);
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesProphecyText))
        {
            reader.Read();
            item.ProphecyText = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesFrameType))
        {
            reader.Read();
            item.FrameType = reader.GetInt32();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesCategory))
        {
            item.Category = ParseCategory(ref reader);
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesX))
        {
            reader.Read();
            item.X = reader.GetInt32();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesY))
        {
            reader.Read();
            item.Y = reader.GetInt32();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesInventoryId))
        {
            reader.Read();
            item.InventoryId = reader.GetString();
        }
        else
        {
            Skip(ref reader);
        }
    }

    private static List<Socket> ParseSocketArray(ref Utf8JsonReader reader)
    {
        reader.Read();
        var results = new List<Socket>();
        while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
        {
            results.Add(ParseSocket(ref reader));
        }
        return results;
    }

    private static Socket ParseSocket(ref Utf8JsonReader reader)
    {
        var socket = new Socket();
        while (reader.Read()
               && reader.TokenType != JsonTokenType.EndObject)
        {
            var propertyName = reader.ValueSpan;
            ParseSocketProperty(ref reader, propertyName, socket);
        }
        return socket;
    }

    private static void ParseSocketProperty(ref Utf8JsonReader reader, ReadOnlySpan<byte> propertyName, Socket socket)
    {
        if (propertyName.SequenceEqual(PropertyNameBytes.BytesAttr))
        {
            reader.Read();
            socket.Attr = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesGroup))
        {
            reader.Read();
            socket.Group = reader.GetInt32();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesSColour))
        {
            reader.Read();
            socket.SColour = reader.GetString();
        }
        else
        {
            Skip(ref reader);
        }
    }

    private static List<Property> ParsePropertyArray(ref Utf8JsonReader reader)
    {
        reader.Read();
        var results = new List<Property>();
        while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
        {
            results.Add(ParseProperty(ref reader));
        }
        return results;
    }

    private static Property ParseProperty(ref Utf8JsonReader reader)
    {
        var property = new Property();
        while (reader.Read()
               && reader.TokenType != JsonTokenType.EndObject)
        {
            var propertyName = reader.ValueSpan;
            ParsePropertyProperty(ref reader, propertyName, property);
        }
        return property;
    }

    private static void ParsePropertyProperty(ref Utf8JsonReader reader, ReadOnlySpan<byte> propertyName, Property property)
    {
        if (propertyName.SequenceEqual(PropertyNameBytes.BytesDisplayMode))
        {
            reader.Read();
            property.DisplayMode = reader.GetInt32();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesName))
        {
            reader.Read();
            property.Name = reader.GetString();
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesValues))
        {
            property.Values = ParseStringArrayArray(ref reader);
        }
        else if (propertyName.SequenceEqual(PropertyNameBytes.BytesType))
        {
            reader.Read();
            property.Type = reader.GetInt32();
        }
        else
        {
            Skip(ref reader);
        }
    }

    private static List<List<string>> ParseStringArrayArray(ref Utf8JsonReader reader)
    {
        reader.Read();
        var results = new List<List<string>>();
        while (reader.Read() 
               && reader.TokenType != JsonTokenType.EndArray)
        {
            results.Add(ParseStringArray(ref reader));
        }
        return results;
    }

    private static List<string> ParseStringArray(ref Utf8JsonReader reader)
    {
        var results = new List<string>();
        reader.Read();
        while (reader.TokenType != JsonTokenType.EndArray)
        {
            var result = reader.TokenType == JsonTokenType.String 
                ? reader.GetString() 
                : reader.GetInt32().ToString();
            results.Add(result);
            reader.Read();
        }
        return results;
    }

    private static Category ParseCategory(ref Utf8JsonReader reader)
    {
        var category = new Category();
        while (reader.Read()
               && reader.TokenType != JsonTokenType.EndObject)
        {
            reader.Read();
            category.Name = reader.GetString();
            reader.Read();
            category.Values = ParseStringArray(ref reader);
        }
        return category;
    }

        private static void Skip(ref Utf8JsonReader reader)
    {
        if (reader.TokenType == JsonTokenType.PropertyName)
        {
            reader.Read();
        }

        if (reader.TokenType == JsonTokenType.StartObject 
            || reader.TokenType == JsonTokenType.StartArray)
        {
            var depth = reader.CurrentDepth;
            while (reader.Read() && depth <= reader.CurrentDepth) { }
        }
    }
}

Sample Input

I use this 6 KB sample file for correctness testing. Actual payloads are as large as 4 MB decompressed.

{
  "next_change_id": "2653-4457-4108-4817-1510",
  "stashes": [
    {
      "id": "6e744b0f76179835e1f681ce81c513ea190cb021b34eaacafe4c3d4f6990395f",
      "public": true,
      "accountName": "5a4oK",
      "lastCharacterName": "Please_remove_volotile",
      "stash": "What i need",
      "stashType": "PremiumStash",
      "items": [
        {
          "verified": false,
          "w": 2,
          "h": 4,
          "ilvl": 71,
          "icon": "http://web.poecdn.com/image/Art/2DItems/Weapons/TwoHandWeapons/Bows/SarkhamsReach.png?scale=1&scaleIndex=0&w=2&h=4&v=f333c2e4005ee20a84270731baa5fa6a3",
          "league": "Hardcore",
          "id": "176b5e6f7af0a5bb4b48d7fdafa47501a179f4ea095815a58c82c4b5244b3cdb",
          "sockets": [
            {
              "group": 0,
              "attr": "D",
              "sColour": "G"
            }
          ],
          "name": "<<set:MS>><<set:M>><<set:S>>Roth's Reach",
          "typeLine": "Recurve Bow",
          "identified": true,
          "note": "~price 10 exa",
          "properties": [
            {
              "name": "Bow",
              "values": [],
              "displayMode": 0
            },
            {
              "name": "Attacks per Second",
              "values": [
                [
                  "1.31",
                  1
                ]
              ],
              "displayMode": 0,
              "type": 13
            }
          ],
          "requirements": [
            {
              "name": "Level",
              "values": [
                [
                  "18",
                  0
                ]
              ],
              "displayMode": 0
            },
            {
              "name": "Dex",
              "values": [
                [
                  "65",
                  0
                ]
              ],
              "displayMode": 1
            }
          ],
          "explicitMods": [
            "68% increased Physical Damage",
            "5% increased Attack Speed",
            "Skills Chain +1 times",
            "30% increased Projectile Speed",
            "34% increased Elemental Damage with Attack Skills"
          ],
          "flavourText": [
            "\"Exiled to the sea; what a joke. \r",
            "I'm more free than I've ever been.\"\r",
            "- Captain Weylam \"Rot-tooth\" Roth of the Black Crest"
          ],
          "frameType": 3,
          "category": {
            "weapons": [
              "bow"
            ]
          },
          "x": 10,
          "y": 0,
          "inventoryId": "Stash1",
          "socketedItems": []
        },
        {
          "verified": false,
          "w": 1,
          "h": 1,
          "ilvl": 0,
          "icon": "http://web.poecdn.com/image/Art/2DItems/Gems/LeapSlam.png?scale=1&scaleIndex=0&w=1&h=1&v=73d0b5f9f1c52f0e0e87f74a80a549ab3",
          "support": false,
          "league": "Hardcore",
          "id": "8d84024bd2f99bd467b671e6915bc999f6e26f512c7f7034f54ff182781198e6",
          "name": "",
          "typeLine": "Leap Slam",
          "identified": true,
          "properties": [
            {
              "name": "Attack, AoE, Movement, Melee",
              "values": [],
              "displayMode": 0
            },
            {
              "name": "Level",
              "values": [
                [
                  "1",
                  0
                ]
              ],
              "displayMode": 0,
              "type": 5
            }
          ],
          "additionalProperties": [
            {
              "name": "Experience",
              "values": [
                [
                  "9569/9569",
                  0
                ]
              ],
              "displayMode": 2,
              "progress": 1
            }
          ],
          "requirements": [
            {
              "name": "Level",
              "values": [
                [
                  "10",
                  0
                ]
              ],
              "displayMode": 0
            },
            {
              "name": "Str",
              "values": [
                [
                  "29",
                  0
                ]
              ],
              "displayMode": 1
            }
          ],
          "nextLevelRequirements": [
            {
              "name": "Level",
              "values": [
                [
                  "13",
                  0
                ]
              ],
              "displayMode": 0
            },
            {
              "name": "Str",
              "values": [
                [
                  "35",
                  0
                ]
              ],
              "displayMode": 1
            }
          ],
          "secDescrText": "Jump into the air, damaging enemies (and knocking back some) with your main hand where you land. Enemies you would land on are pushed out of the way. Requires an axe, mace, sword or staff. Cannot be supported by Multistrike.",
          "explicitMods": [
            "20% chance to Knock Enemies Back on hit"
          ],
          "descrText": "Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.",
          "frameType": 4,
          "category": {
            "gems": []
          },
          "x": 0,
          "y": 1,
          "inventoryId": "Stash2"
        }
      ]
    }
  ]
}

Sample Output

The output is a populated C# model of type RootObject. Below is a unit test that uses the sample input above and verifies that the model was populated correctly.

    [TestFixture]
    public class JsonParserServiceTests
    {
        [Test]
        public void TestParse_ProducesCorrectValues()
        {
            // Arrange
            var json = File.ReadAllText("TestFiles\\small.json");
            var sut = new FastJsonParserService();

            // Act
            var actual = sut.Parse(json);

            // Assert
            Assert.AreEqual("2653-4457-4108-4817-1510", actual.NextChangeId);
            Assert.AreEqual(1, actual.Stashes.Count);
            AssertStash0(actual.Stashes[0]);
        }

        private static void AssertStash0(Stash stash)
        {
            Assert.AreEqual("6e744b0f76179835e1f681ce81c513ea190cb021b34eaacafe4c3d4f6990395f", stash.Id);
            Assert.AreEqual("5a4oK", stash.AccountName);
            Assert.AreEqual("Please_remove_volotile", stash.LastCharacterName);
            Assert.AreEqual("What i need", stash.Name);
            Assert.AreEqual("PremiumStash", stash.StashType);
            Assert.AreEqual(2, stash.Items.Count);
            AssertStash0Item0(stash.Items[0]);
        }

        private static void AssertStash0Item0(Item item)
        {
            Assert.AreEqual(false, item.Verified);
            Assert.AreEqual(2, item.W);
            Assert.AreEqual(4, item.H);
            Assert.AreEqual(71, item.Ilvl);
            Assert.AreEqual("http://web.poecdn.com/image/Art/2DItems/Weapons/TwoHandWeapons/Bows/SarkhamsReach.png?scale=1&scaleIndex=0&w=2&h=4&v=f333c2e4005ee20a84270731baa5fa6a3", item.Icon);
            Assert.AreEqual("Hardcore", item.League);
            Assert.AreEqual("176b5e6f7af0a5bb4b48d7fdafa47501a179f4ea095815a58c82c4b5244b3cdb", item.Id);
            Assert.AreEqual(1, item.Sockets.Count);
            AssertStash0Item0Sockets0(item.Sockets[0]);
            Assert.AreEqual("<<set:MS>><<set:M>><<set:S>>Roth's Reach", item.Name);
            Assert.AreEqual(true, item.Identified);
            Assert.AreEqual("~price 10 exa", item.Note);
            Assert.AreEqual(2, item.Properties.Count);
            AssertStash0Item0Properties0(item.Properties.FirstOrDefault(p => p.Name.Equals("Bow")));
            AssertStash0Item0Properties1(item.Properties.FirstOrDefault(p => p.Name.Equals("Attacks per Second")));
            Assert.AreEqual(2, item.Requirements.Count);
            AssertStash0Item0Requirements0(item.Requirements[0]);
            Assert.AreEqual(5, item.ExplicitMods.Count);
            Assert.NotNull(item.ExplicitMods.FirstOrDefault(e => e.Equals("68% increased Physical Damage")));
            Assert.AreEqual(3, item.FlavourText.Count);
            Assert.NotNull(item.FlavourText.FirstOrDefault(e => e.Equals("\"Exiled to the sea; what a joke. \r")));
            Assert.AreEqual(3, item.FrameType);
            AssertStash0Item0Category(item.Category);
            Assert.AreEqual(10, item.X);
            Assert.AreEqual(0, item.Y);
            Assert.AreEqual("Stash1", item.InventoryId);
        }

        private static void AssertStash0Item0Sockets0(Socket socket)
        {
            Assert.AreEqual(0, socket.Group);
            Assert.AreEqual("D", socket.Attr);
            Assert.AreEqual("G", socket.SColour);
        }

        private static void AssertStash0Item0Properties0(Property property)
        {
            CollectionAssert.IsEmpty(property.Values);
            Assert.AreEqual(0, property.DisplayMode);
        }

        private static void AssertStash0Item0Properties1(Property property)
        {
            Assert.AreEqual("1.31", property.Values[0][0]);
            Assert.AreEqual("1", property.Values[0][1]);
            Assert.AreEqual(0, property.DisplayMode);
        }

        private static void AssertStash0Item0Requirements0(Property requirement)
        {
            Assert.AreEqual("Level", requirement.Name);
            Assert.AreEqual(1, requirement.Values.Count);
            Assert.AreEqual(2, requirement.Values[0].Count);
            Assert.AreEqual("18", requirement.Values[0][0]);
            Assert.AreEqual("0", requirement.Values[0][1]);
            Assert.AreEqual(0, requirement.DisplayMode);
        }

        private static void AssertStash0Item0Category((string, List<string>) category)
        {
            Assert.AreEqual("weapons", category.Item1);
            Assert.AreEqual(1, category.Item2.Count);
            Assert.AreEqual("bow", category.Item2[0]);
        }
    }
\$\endgroup\$
  • \$\begingroup\$ Just wondering, why not compare your implementation to Json.NET in your tests? \$\endgroup\$ – Brad M Mar 3 at 13:42
  • 1
    \$\begingroup\$ @t3chb0t Sorry it took me so long to respond due to work. I made a minimal project and pushed it to GitHub: github.com/d2erusher/JsonParserCodeReview. It has everything you see in the question, plus some models and an additional test. You're right - I am using the pre-release version of .NET Core 3.0, and Utf8JsonReader does come with it. I think you need Visual Studio 2019 Preview in order to work with it, but I could be wrong about that. \$\endgroup\$ – Rainbolt Mar 5 at 5:29
  • 2
    \$\begingroup\$ This is embarrassing. I had already made some progress towards making the code more DRY, and accidentally pushed the improved code to GitHub. I wasn't trying to hide my progress, but I wanted to give the community a chance to post answers before I posted a self-answer. I updated the GitHub repo to have the same version of the code as the question. \$\endgroup\$ – Rainbolt Mar 5 at 5:42
  • 1
    \$\begingroup\$ You say that you tried the JsonConvert.DeserializeObject API, but have you also tried the one that supports streams: JsonSerializer.Deserialize - you can pass it a JsonReader. This should be significantly faster then loading a file as a string first and then parsing it. \$\endgroup\$ – t3chb0t Mar 5 at 9:05
  • 1
    \$\begingroup\$ @t3chb0t I hadn't tried it before, but now I have. The benchmark was a 4 MB file, parsed 100 times (with no reusing the file text in between). JsonSerializer.Deserialize took 122ms per iteration, JsonSerializer.DeserializeObject 131 ms per iteration, and Utf8JsonReader 116 ms per iteration. Looks like you're right. The benchmarking code is on the GitHub linked in the question in JsonParserPerformanceBenchmark.cs. \$\endgroup\$ – Rainbolt Mar 6 at 3:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.