Skip to content

C sharp and JSON

  • At least as of mid 2024, the built-in System.Text.Json stuff seems pretty powerful.
  • The official Microsoft documentation is good (although sometimes long).
  • Note to self: I worked somewhat extensively with JSON stuff for Skeleseller, so if I’m ever looking for code in the future, check there. To anyone else reading this, the source is closed at the time of writing. 😢

Serializing read-only properties that are collections

Section titled Serializing read-only properties that are collections

I had code like this:

public class LocalSaveData
{
public int FpsLimit { get; set; } = GameConstants.FpsLimitMax;
public Dictionary<VolumeSliderId, float> VolumeSliderValues { get; } =
new()
{
{ VolumeSliderId.Master, 0.6f },
{ VolumeSliderId.Music, 0.8f },
{ VolumeSliderId.Effects, 0.6f },
};
}

The way this is written, if serialized/deserialized, the Dictionary’s values will always be what’s hard-coded there. To fix it, there are two options:

  • Add a set; to the property. This will result in linting rule CA2227 telling you not to do that.
  • Preferred: add [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] just above the Dictionary (reference)
ERROR: System.InvalidOperationException: Each parameter in the deserialization constructor on type 'Game.Src.Quests.Quest' must bind to an object property or field on deserialization. Each parameter name must match with a property or field on the object. Fields are only considered when 'JsonSerializerOptions.IncludeFields' is enabled. The match can be case-insensitive.

This error is sort of hard to decipher at first. It means that you’re trying to load data from a JSON file that doesn’t correspond to a constructor or field. For example, suppose I have this in JSON:

"Quest": {
"Objectives": {
"MediumGreenPotion": 6,
"BigLifePotion": 2,
"SmallBluePotion": 11
},
"Progress": {
"MediumGreenPotion": 6,
"BigLifePotion": 2,
"SmallBluePotion": 10
},
"RewardPoints": 100
},

…and this constructor in C#:

public class Quest
{
[JsonConstructor]
public Quest(Dictionary<ItemId, int> objectives, int rewardSize)
{
// ...
}
}

There are two problems above:

  • The JSON has the name RewardPoints but C# uses the name rewardSize. You can fix this simply by renaming rewardSizerewardPoints (since, as the error mentions, the match is case-insensitive).
  • The JSON has a Progress dictionary but the Quest constructor doesn’t take that in. For this fix, it made more sense to make a Progress property (public Dictionary<ItemId, int> Progress { get; init; }).

In conclusion, C# just needs to know how to deserialize what’s found in JSON either via a constructor/property/field.

A custom converter is serializing incorrectly

Section titled A custom converter is serializing incorrectly
{
"$type": "HeldItemComponent",
"Item":
"id": 1 // ← ❌ This isn't valid JSON
}

This is the fault of the custom converter I wrote:

public class ItemConverter : JsonConverter<Item>
{
public override Item Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return Item.Create((ItemId)reader.GetInt32());
}
public override void Write(Utf8JsonWriter writer, Item value, JsonSerializerOptions options)
{
writer.WriteNumber(System.Text.Encoding.UTF8.GetBytes("id"), (int)value.Id);
}
}

Depending on what you want, there are two different fixes:

  • If the JSON you want is "Item": { "id": 1 }, then surround the call to writer.WriteNumber with writer.WriteStartObject(); and writer.WriteEndObject();
    • This also requires a change to the reader (see below); you generally need to loop on reader.Read() and check for when reader.TokenType is JsonTokenType.StartObject, JsonTokenType.EndObject, or JsonTokenType.PropertyName.
  • If the JSON you want is "Item": 1, then replace writer.WriteNumber with writer.WriteNumberValue.

Similarly, if you wanted to write arrays, you need to explicitly call WriteStartArray and WriteEndArray.

This is kind of ridiculous, but here’s how you would read the { "id": 1 } object. I didn’t need a loop since I only write one value:

public override Item Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
reader.Read();
// Get the key
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
// Ensure its value is correct
string? propertyName = reader.GetString();
if (propertyName != "id")
{
throw new JsonException();
}
reader.Read();
// Get the value
ItemId itemId = (ItemId)reader.GetInt32();
// Ensure we found the end of the object
reader.Read();
if (reader.TokenType == JsonTokenType.EndObject)
{
return Item.Create(itemId);
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, Item value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteNumber(System.Text.Encoding.UTF8.GetBytes("id"), (int)value.Id);
writer.WriteEndObject();
}