Deserializing JSON with Utf8JsonReader












0












$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



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]);
}
}









share|improve this question







New contributor




Rainbolt is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.







$endgroup$

















    0












    $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



    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]);
    }
    }









    share|improve this question







    New contributor




    Rainbolt is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
    Check out our Code of Conduct.







    $endgroup$















      0












      0








      0





      $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



      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]);
      }
      }









      share|improve this question







      New contributor




      Rainbolt is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.







      $endgroup$




      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



      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]);
      }
      }






      c# .net json






      share|improve this question







      New contributor




      Rainbolt is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.











      share|improve this question







      New contributor




      Rainbolt is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.









      share|improve this question




      share|improve this question






      New contributor




      Rainbolt is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.









      asked 17 mins ago









      RainboltRainbolt

      1013




      1013




      New contributor




      Rainbolt is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.





      New contributor





      Rainbolt is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.






      Rainbolt is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.






















          0






          active

          oldest

          votes











          Your Answer





          StackExchange.ifUsing("editor", function () {
          return StackExchange.using("mathjaxEditing", function () {
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          });
          });
          }, "mathjax-editing");

          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "196"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          autoActivateHeartbeat: false,
          convertImagesToLinks: false,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });






          Rainbolt is a new contributor. Be nice, and check out our Code of Conduct.










          draft saved

          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f214625%2fdeserializing-json-with-utf8jsonreader%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown

























          0






          active

          oldest

          votes








          0






          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes








          Rainbolt is a new contributor. Be nice, and check out our Code of Conduct.










          draft saved

          draft discarded


















          Rainbolt is a new contributor. Be nice, and check out our Code of Conduct.













          Rainbolt is a new contributor. Be nice, and check out our Code of Conduct.












          Rainbolt is a new contributor. Be nice, and check out our Code of Conduct.
















          Thanks for contributing an answer to Code Review Stack Exchange!


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          Use MathJax to format equations. MathJax reference.


          To learn more, see our tips on writing great answers.




          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f214625%2fdeserializing-json-with-utf8jsonreader%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown





















































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown

































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown







          Popular posts from this blog

          How to reconfigure Docker Trusted Registry 2.x.x to use CEPH FS mount instead of NFS and other traditional...

          is 'sed' thread safe

          How to make a Squid Proxy server?