C
C#3w ago
FusedQyou

✅ Serializing/Deserializing a Dictionary with a struct key not working

I have been pulling my hair out to support this library that uses a struct key. Since the Dictionary uses a weird key, I have to change the serialization. The class is as follows:
public sealed class BCCCodebase
{
public Collection<FeatureBase> Features { get; } = [];
public Dictionary<BCCNamespace, BCCCodebase> Namespaces { get; } = [];
}

public readonly struct BCCNamespace(
bool isStrict,
string? name)
: IEquatable<BCCNamespace>
{
public bool IsStrict { get; } = isStrict;
public string? Name { get; } = name;

// Equality methods follow
}

// This class is additionally inherited with various features.
public abstract class BCCFeatureBase(
bool isPrivate)
: FeatureBase
{
public bool IsPrivate { get; } = isPrivate;
}
public abstract class FeatureBase
{
public JavadocComment? Comment { get; internal set; }
}
public sealed class BCCCodebase
{
public Collection<FeatureBase> Features { get; } = [];
public Dictionary<BCCNamespace, BCCCodebase> Namespaces { get; } = [];
}

public readonly struct BCCNamespace(
bool isStrict,
string? name)
: IEquatable<BCCNamespace>
{
public bool IsStrict { get; } = isStrict;
public string? Name { get; } = name;

// Equality methods follow
}

// This class is additionally inherited with various features.
public abstract class BCCFeatureBase(
bool isPrivate)
: FeatureBase
{
public bool IsPrivate { get; } = isPrivate;
}
public abstract class FeatureBase
{
public JavadocComment? Comment { get; internal set; }
}
My test is written as followed (some code removed for readability):
internal sealed class BCCParserTests
{
private JsonSerializerOptions _jsonSerializerOptions;

[OneTimeSetUp]
public void SetUp()
{
this._jsonSerializerOptions = new();
this._jsonSerializerOptions.Converters.Add(new BCCNamespaceJsonConverter());
}

[Test]
[TestCase("Parsing_File_Returns_Features_Input1")]
public async Task Parsing_File_Returns_Features(string fileName)
{
var parser = ActivatorUtilities.CreateInstance<BCCParser>(this._serviceProvider);
await parser.ParseFileAsync($"TestFiles/BCC/{fileName}.acs");
var featuresJson = JsonSerializer.Serialize(parser.BaseCodebase, this._jsonSerializerOptions);
_ = await VerifyJson(featuresJson, this._verifySettings);
}
}
internal sealed class BCCParserTests
{
private JsonSerializerOptions _jsonSerializerOptions;

[OneTimeSetUp]
public void SetUp()
{
this._jsonSerializerOptions = new();
this._jsonSerializerOptions.Converters.Add(new BCCNamespaceJsonConverter());
}

[Test]
[TestCase("Parsing_File_Returns_Features_Input1")]
public async Task Parsing_File_Returns_Features(string fileName)
{
var parser = ActivatorUtilities.CreateInstance<BCCParser>(this._serviceProvider);
await parser.ParseFileAsync($"TestFiles/BCC/{fileName}.acs");
var featuresJson = JsonSerializer.Serialize(parser.BaseCodebase, this._jsonSerializerOptions);
_ = await VerifyJson(featuresJson, this._verifySettings);
}
}
The test here fails because Features is incorrectly filled with just the comment from FeatureBase and the rest seems to be skipped. Before this I didn't serialize the object to JSON so it even breaks without BCCNamespaceJsonConverter. How do I support this?
4 Replies
FusedQyou
FusedQyouOP3w ago
Converter:
internal sealed class BCCNamespaceJsonConverter : JsonConverter<Dictionary<BCCNamespace, BCCCodebase>>
{
/// <inheritdoc />
public override Dictionary<BCCNamespace, BCCCodebase>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return null!;
}

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, Dictionary<BCCNamespace, BCCCodebase> value, JsonSerializerOptions options)
{
writer.WriteStartObject();

foreach (var kvp in value)
{
var key = JsonSerializer.Serialize(kvp.Key, options);
writer.WritePropertyName(key);

JsonSerializer.Serialize(writer, kvp.Value, options);
}

writer.WriteEndObject();
}
}
internal sealed class BCCNamespaceJsonConverter : JsonConverter<Dictionary<BCCNamespace, BCCCodebase>>
{
/// <inheritdoc />
public override Dictionary<BCCNamespace, BCCCodebase>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return null!;
}

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, Dictionary<BCCNamespace, BCCCodebase> value, JsonSerializerOptions options)
{
writer.WriteStartObject();

foreach (var kvp in value)
{
var key = JsonSerializer.Serialize(kvp.Key, options);
writer.WritePropertyName(key);

JsonSerializer.Serialize(writer, kvp.Value, options);
}

writer.WriteEndObject();
}
}
Verifier objects. Example 1. This result is expected:
{
Features: [
{
Type: int,
Values: [
{
Name: FOO
},
{
Name: BAR,
Value: 5
},
{
Name: BAZ
}
],
IsPrivate: false
},
{
Name: Testing,
Type: int,
Values: [
{
Name: FOO
},
{
Name: BAR
},
{
Name: BAZ
}
],
IsPrivate: true
}
]
}
{
Features: [
{
Type: int,
Values: [
{
Name: FOO
},
{
Name: BAR,
Value: 5
},
{
Name: BAZ
}
],
IsPrivate: false
},
{
Name: Testing,
Type: int,
Values: [
{
Name: FOO
},
{
Name: BAR
},
{
Name: BAZ
}
],
IsPrivate: true
}
]
}
But this is the actual:
{
Features: [
{
Comment: null
},
{
Comment: null
}
]
}
{
Features: [
{
Comment: null
},
{
Comment: null
}
]
}
Example 2 with the actual dictionary. Current verifier object:
{
Namespaces: {
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: foo,
DefaultValue: 5
}
]
},
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: bar,
DefaultValue: 5
}
]
},
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: baz,
DefaultValue: 5
}
]
},
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: qux,
DefaultValue: 5
}
]
},
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: quux,
DefaultValue: 5
}
]
},
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: garply,
DefaultValue: 5
}
],
Namespaces: {
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: corge,
DefaultValue: 5
}
],
Namespaces: {
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: grault,
DefaultValue: 5
}
]
}
}
}
}
},
LexerTest.BCC.Parser.BCCNamespace: {
Namespaces: {
LexerTest.BCC.Parser.BCCNamespace: {
Namespaces: {
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: waldo,
DefaultValue: 5
}
]
}
}
}
}
}
}
}
{
Namespaces: {
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: foo,
DefaultValue: 5
}
]
},
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: bar,
DefaultValue: 5
}
]
},
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: baz,
DefaultValue: 5
}
]
},
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: qux,
DefaultValue: 5
}
]
},
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: quux,
DefaultValue: 5
}
]
},
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: garply,
DefaultValue: 5
}
],
Namespaces: {
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: corge,
DefaultValue: 5
}
],
Namespaces: {
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: grault,
DefaultValue: 5
}
]
}
}
}
}
},
LexerTest.BCC.Parser.BCCNamespace: {
Namespaces: {
LexerTest.BCC.Parser.BCCNamespace: {
Namespaces: {
LexerTest.BCC.Parser.BCCNamespace: {
Features: [
{
Type: int,
Name: waldo,
DefaultValue: 5
}
]
}
}
}
}
}
}
}
See this initially works fine, but it skips the keys. Now adding the actual serialization, the key now works but it omits basically everything else:
{
Namespaces: {
{"IsStrict":true,"Name":"Foo"}: {
Features: [
{
Comment: null
}
]
},
{"IsStrict":true,"Name":null}: {
Features: [
{
Comment: null
}
]
},
{"IsStrict":false,"Name":null}: {
Features: [
{
Comment: null
}
]
},
{"IsStrict":false,"Name":"Bar"}: {
Features: [
{
Comment: null
}
]
},
{"IsStrict":false,"Name":"Foo"}: {
Features: [
{
Comment: null
}
]
},
{"IsStrict":false,"Name":"Baz"}: {
Features: [
{
Comment: null
}
],
Namespaces: {
{"IsStrict":false,"Name":"Qux"}: {
Features: [
{
Comment: null
}
],
Namespaces: {
{"IsStrict":false,"Name":"Quux"}: {
Features: [
{
Comment: null
}
]
}
}
}
}
},
{"IsStrict":false,"Name":"Garply"}: {
Namespaces: {
{"IsStrict":false,"Name":"Waldo"}: {
Namespaces: {
{"IsStrict":false,"Name":"Fred"}: {
Features: [
{
Comment: null
}
]
}
}
}
}
}
}
}
{
Namespaces: {
{"IsStrict":true,"Name":"Foo"}: {
Features: [
{
Comment: null
}
]
},
{"IsStrict":true,"Name":null}: {
Features: [
{
Comment: null
}
]
},
{"IsStrict":false,"Name":null}: {
Features: [
{
Comment: null
}
]
},
{"IsStrict":false,"Name":"Bar"}: {
Features: [
{
Comment: null
}
]
},
{"IsStrict":false,"Name":"Foo"}: {
Features: [
{
Comment: null
}
]
},
{"IsStrict":false,"Name":"Baz"}: {
Features: [
{
Comment: null
}
],
Namespaces: {
{"IsStrict":false,"Name":"Qux"}: {
Features: [
{
Comment: null
}
],
Namespaces: {
{"IsStrict":false,"Name":"Quux"}: {
Features: [
{
Comment: null
}
]
}
}
}
}
},
{"IsStrict":false,"Name":"Garply"}: {
Namespaces: {
{"IsStrict":false,"Name":"Waldo"}: {
Namespaces: {
{"IsStrict":false,"Name":"Fred"}: {
Features: [
{
Comment: null
}
]
}
}
}
}
}
}
}
So you see as soon as I change the test to serialize the JSON, it is unable to properly serialize the features. If I use verifier directly it works fine, but I have to pass this custom serializer for my Dictionary. For clarification, the test initially looked like this to get the working objects minus the keys:
public async Task Parsing_File_Returns_Features(string fileName)
{
// Arrange
var parser = ActivatorUtilities.CreateInstance<BCCParser>(this._serviceProvider);

// Act
await parser.ParseFileAsync($"TestFiles/BCC/{fileName}.acs");

// Assert
var features = parser.BaseCodebase;
_ = await Verify(features, this._verifySettings);

// Cleanup
parser.Clear();
}
public async Task Parsing_File_Returns_Features(string fileName)
{
// Arrange
var parser = ActivatorUtilities.CreateInstance<BCCParser>(this._serviceProvider);

// Act
await parser.ParseFileAsync($"TestFiles/BCC/{fileName}.acs");

// Assert
var features = parser.BaseCodebase;
_ = await Verify(features, this._verifySettings);

// Cleanup
parser.Clear();
}
So no serialization
ero
ero3w ago
well the comments problem is because Features is declared as Collection<FeatureBase>, and FeatureBase simply only contains Comment. you'll need to mark FeatureBase with [JsonDerivedType(typeof(BCCFeatureBase))] for that to work. but i see a much more glaring issue, which is the keys. json keys cannot be anything but strings, so {"IsStrict":true,"Name":"Foo"}: { absolutely cannot work
FusedQyou
FusedQyouOP3w ago
It was mostly for testing but on second thought I do want to eventually have this serialize properly so it can be used elsewhere. Perhaps it's better if the actual key here is fixed I didn't know JsonDerivedType was a thing I think it's best if I simplify this code so it just uses the namespace name instead, or something. I probably do end up requiring the strict boolean so I suppose in this case the best idea is to convert the struct into a string? I suppose it would work if I were to make the key JSON. Otherwise maybe create a comma delimited string Otherwise I'd have to get rid of the dictionary in general which I would rather not have $close
MODiX
MODiX3w ago
If you have no further questions, please use /close to mark the forum thread as answered
Want results from more Discord servers?
Add your server