Deserializing JSON with Utf8JsonReader
$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]);
}
}
c# .net json
New contributor
$endgroup$
add a comment |
$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]);
}
}
c# .net json
New contributor
$endgroup$
add a comment |
$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]);
}
}
c# .net json
New contributor
$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
c# .net json
New contributor
New contributor
New contributor
asked 17 mins ago
RainboltRainbolt
1013
1013
New contributor
New contributor
add a comment |
add a comment |
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.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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.
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.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
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