From adfdf2760825760112b35245837edbb8fabeefa3 Mon Sep 17 00:00:00 2001 From: Rasmus Soeborg Date: Mon, 16 Sep 2024 11:56:19 +0200 Subject: [PATCH 1/6] feat: changed 'TypeIdDecodedConverter' and 'TypeIdConverter' into factory classes that can handle nullability correctly --- .../TypeIdConverter.cs | 29 ----- .../TypeIdConverterFactory.cs | 98 +++++++++++++++++ .../TypeIdDecodedConverter.cs | 50 --------- .../TypeIdDecodedConverterFactory.cs | 103 ++++++++++++++++++ 4 files changed, 201 insertions(+), 79 deletions(-) delete mode 100644 src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdConverter.cs create mode 100644 src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdConverterFactory.cs delete mode 100644 src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverter.cs create mode 100644 src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverterFactory.cs diff --git a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdConverter.cs b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdConverter.cs deleted file mode 100644 index a479d77..0000000 --- a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdConverter.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace FastIDs.TypeId.Serialization.SystemTextJson; - -public class TypeIdConverter : JsonConverter -{ - public override TypeId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var val = reader.GetString(); - return val is not null ? TypeId.Parse(val) : default; - } - - public override void Write(Utf8JsonWriter writer, TypeId value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.ToString()); - } - - public override void WriteAsPropertyName(Utf8JsonWriter writer, TypeId value, JsonSerializerOptions options) - { - writer.WritePropertyName(value.ToString()); - } - - public override TypeId ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var val = reader.GetString(); - return val is not null ? TypeId.Parse(val) : default; - } -} \ No newline at end of file diff --git a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdConverterFactory.cs b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdConverterFactory.cs new file mode 100644 index 0000000..f331e65 --- /dev/null +++ b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdConverterFactory.cs @@ -0,0 +1,98 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace FastIDs.TypeId.Serialization.SystemTextJson; + +public class TypeIdConverterFactory : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert == typeof(TypeId) || typeToConvert == typeof(TypeId?); + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + // Create a converter for the specific type + return (JsonConverter?)Activator.CreateInstance( + typeof(TypeIdConverter<>).MakeGenericType(typeToConvert), + BindingFlags.Instance | BindingFlags.Public, + null, + null, + null) ?? throw new Exception("Could not create converter"); + } + + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", + Justification = "Instantiated via reflection")] + private sealed class TypeIdConverter : JsonConverter + { + // Determine if the type is nullable + private static readonly bool IsNullable = Nullable.GetUnderlyingType(typeof(T)) != null; + + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Handle null values + if (reader.TokenType == JsonTokenType.Null) + { + if (IsNullable) + return default; + throw new JsonException($"Cannot convert null to {typeof(T)}."); + } + + var val = reader.GetString(); + if (!string.IsNullOrEmpty(val)) + { + var typeId = TypeId.Parse(val); + return (T)(object)typeId; + } + + throw new JsonException($"Expected a non-null, non-empty string for {typeof(T)}."); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + // Handle null values + if (value == null) + { + if (IsNullable) + { + writer.WriteNullValue(); + return; + } + + throw new JsonException($"Cannot write null value for {typeof(T)}."); + } + + var typedValue = (TypeId)(object)value; + writer.WriteStringValue(typedValue.ToString()); + } + + public override T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) + { + // Read property name as string + if (reader.TokenType == JsonTokenType.PropertyName) + { + var val = reader.GetString(); + if (!string.IsNullOrEmpty(val)) + { + var typeId = TypeId.Parse(val); + return (T)(object)typeId; + } + + throw new JsonException($"Expected a non-null, non-empty string for property name of {typeof(T)}."); + } + + throw new JsonException($"Expected a property name token for {typeof(T)}."); + } + + public override void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + if (value == null) throw new JsonException($"Cannot write null as a property name for {typeof(T)}."); + + var typedValue = (TypeId)(object)value; + writer.WritePropertyName(typedValue.ToString()); + } + } +} \ No newline at end of file diff --git a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverter.cs b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverter.cs deleted file mode 100644 index 59ad3fc..0000000 --- a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverter.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace FastIDs.TypeId.Serialization.SystemTextJson; - -public class TypeIdDecodedConverter : JsonConverter -{ - public override TypeIdDecoded Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return ReadTypeId(ref reader); - } - - public override void Write(Utf8JsonWriter writer, TypeIdDecoded value, JsonSerializerOptions options) - { - var totalLength = value.Type.Length + 1 + 26; - Span buffer = stackalloc char[totalLength]; - - CopyValueToBuffer(value, buffer); - - writer.WriteStringValue(buffer); - } - - public override TypeIdDecoded ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return ReadTypeId(ref reader); - } - - public override void WriteAsPropertyName(Utf8JsonWriter writer, TypeIdDecoded value, JsonSerializerOptions options) - { - var totalLength = value.Type.Length + 1 + 26; - Span buffer = stackalloc char[totalLength]; - - CopyValueToBuffer(value, buffer); - - writer.WritePropertyName(buffer); - } - - private static TypeIdDecoded ReadTypeId(ref Utf8JsonReader reader) - { - var val = reader.GetString(); - return val is not null ? TypeId.Parse(val).Decode() : default; - } - - private static void CopyValueToBuffer(in TypeIdDecoded value, Span buffer) - { - value.Type.AsSpan().CopyTo(buffer); - buffer[value.Type.Length] = '_'; - value.GetSuffix(buffer[(value.Type.Length + 1)..]); - } -} \ No newline at end of file diff --git a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverterFactory.cs b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverterFactory.cs new file mode 100644 index 0000000..a6adc21 --- /dev/null +++ b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverterFactory.cs @@ -0,0 +1,103 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace FastIDs.TypeId.Serialization.SystemTextJson; + +public class TypeIdDecodedConverterFactory : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert == typeof(TypeIdDecoded) || typeToConvert == typeof(TypeIdDecoded?); + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + // Create a converter for the specific type + return (JsonConverter)Activator.CreateInstance( + typeof(TypeIdDecodedConverter<>).MakeGenericType(typeToConvert), + BindingFlags.Instance | BindingFlags.Public, + binder: null, + args: null, + culture: null); + } + + private class TypeIdDecodedConverter : JsonConverter + { + // Determine if the type is nullable + private static readonly bool IsNullable = Nullable.GetUnderlyingType(typeof(T)) != null; + + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Handle null values + if (reader.TokenType == JsonTokenType.Null) + { + if (IsNullable) + { + return default; + } + else + { + throw new JsonException($"Cannot convert null to {typeof(T)}."); + } + } + + var val = reader.GetString(); + if (!string.IsNullOrEmpty(val)) + { + var decoded = TypeId.Parse(val).Decode(); + return (T)(object)decoded; + } + + throw new JsonException($"Expected a non-null, non-empty string for {typeof(T)}."); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + // Handle null values + if (value == null) + { + writer.WriteNullValue(); + return; + } + + var typedValue = (TypeIdDecoded)(object)value; + + var totalLength = typedValue.Type.Length + 1 + 26; + Span buffer = stackalloc char[totalLength]; + + CopyValueToBuffer(typedValue, buffer); + + writer.WriteStringValue(buffer); + } + + public override T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return Read(ref reader, typeToConvert, options); + } + + public override void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + if (value == null) + { + throw new JsonException($"Cannot write null as a property name for {typeof(T)}."); + } + + var typedValue = (TypeIdDecoded)(object)value; + + var totalLength = typedValue.Type.Length + 1 + 26; + Span buffer = stackalloc char[totalLength]; + + CopyValueToBuffer(typedValue, buffer); + + writer.WritePropertyName(buffer); + } + + private static void CopyValueToBuffer(in TypeIdDecoded value, Span buffer) + { + value.Type.AsSpan().CopyTo(buffer); + buffer[value.Type.Length] = '_'; + value.GetSuffix(buffer[(value.Type.Length + 1)..]); + } + } +} \ No newline at end of file From eb3a8fcadca12bcad57102364b32930df3b09131 Mon Sep 17 00:00:00 2001 From: Rasmus Soeborg Date: Mon, 16 Sep 2024 11:57:29 +0200 Subject: [PATCH 2/6] test: Add tests for nullable handling in TypeIdDecodedSerialization - Added tests to handle serialization and deserialization of null properties in TypeIdDecodedContainer objects. - Added tests for collections and dictionaries with null values. --- .../TypeIdDecodedSerializationTests.cs | 93 +++++++++++++++++-- 1 file changed, 85 insertions(+), 8 deletions(-) diff --git a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson.Tests/TypeIdDecodedSerializationTests.cs b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson.Tests/TypeIdDecodedSerializationTests.cs index 8aafad2..c58f024 100644 --- a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson.Tests/TypeIdDecodedSerializationTests.cs +++ b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson.Tests/TypeIdDecodedSerializationTests.cs @@ -26,15 +26,34 @@ public void TypeId_NestedProperty_Serialized() json.Should().Be($"{{\"Id\":\"{TypeIdStr}\",\"Value\":42}}"); } + [Test] + public void TypeId_NestedProperty_Null_Serialized() + { + var obj = new TypeIdDecodedContainer(null, 42); + var json = JsonSerializer.Serialize(obj, _options); + + json.Should().Be("{\"Id\":null,\"Value\":42}"); + } + [Test] public void TypeId_Collection_Serialized() { - var obj = new TypeIdDecodedArrayContainer(new[] { TypeId.Parse(TypeIdStr).Decode(), TypeId.Parse("prefix_0123456789abcdefghjkmnpqrs").Decode() }); + var obj = new TypeIdDecodedArrayContainer(new TypeIdDecoded?[] + { TypeId.Parse(TypeIdStr).Decode(), TypeId.Parse("prefix_0123456789abcdefghjkmnpqrs").Decode() }); var json = JsonSerializer.Serialize(obj, _options); json.Should().Be($"{{\"Items\":[\"{TypeIdStr}\",\"prefix_0123456789abcdefghjkmnpqrs\"]}}"); } + [Test] + public void TypeId_Collection_WithNull_Serialized() + { + var obj = new TypeIdDecodedArrayContainer(new TypeIdDecoded?[] { TypeId.Parse(TypeIdStr).Decode(), null }); + var json = JsonSerializer.Serialize(obj, _options); + + json.Should().Be($"{{\"Items\":[\"{TypeIdStr}\",null]}}"); + } + [Test] public void TypeId_Plain_Deserialized() { @@ -43,20 +62,49 @@ public void TypeId_Plain_Deserialized() typeId.Should().Be(TypeId.Parse(TypeIdStr).Decode()); } + [Test] + public void TypeId_Plain_Null_Deserialized() + { + var typeId = JsonSerializer.Deserialize("null", _options); + + typeId.Should().BeNull(); + } + [Test] public void TypeId_NestedProperty_Deserialized() { - var obj = JsonSerializer.Deserialize($"{{\"Id\":\"{TypeIdStr}\",\"Value\":42}}", _options); + var obj = JsonSerializer.Deserialize($"{{\"Id\":\"{TypeIdStr}\",\"Value\":42}}", + _options); obj.Should().Be(new TypeIdDecodedContainer(TypeId.Parse(TypeIdStr).Decode(), 42)); } + [Test] + public void TypeId_NestedProperty_Null_Deserialized() + { + var obj = JsonSerializer.Deserialize("{\"Id\":null,\"Value\":42}", _options); + + obj.Should().Be(new TypeIdDecodedContainer(null, 42)); + } + [Test] public void TypeId_Collection_Deserialized() { - var obj = JsonSerializer.Deserialize($"{{\"Items\":[\"{TypeIdStr}\",\"prefix_0123456789abcdefghjkmnpqrs\"]}}", _options); + var obj = JsonSerializer.Deserialize( + $"{{\"Items\":[\"{TypeIdStr}\",\"prefix_0123456789abcdefghjkmnpqrs\"]}}", _options); - obj.Should().BeEquivalentTo(new TypeIdDecodedArrayContainer(new[] { TypeId.Parse(TypeIdStr).Decode(), TypeId.Parse("prefix_0123456789abcdefghjkmnpqrs").Decode() })); + obj.Should().BeEquivalentTo(new TypeIdDecodedArrayContainer(new TypeIdDecoded?[] + { TypeId.Parse(TypeIdStr).Decode(), TypeId.Parse("prefix_0123456789abcdefghjkmnpqrs").Decode() })); + } + + [Test] + public void TypeId_Collection_WithNull_Deserialized() + { + var obj = JsonSerializer.Deserialize($"{{\"Items\":[\"{TypeIdStr}\",null]}}", + _options); + + obj.Should().BeEquivalentTo(new TypeIdDecodedArrayContainer(new TypeIdDecoded?[] + { TypeId.Parse(TypeIdStr).Decode(), null })); } [Test] @@ -69,15 +117,44 @@ public void TypeId_DictionaryKey_Serialized() json.Should().Be($"{{\"{TypeIdStr}\":\"Test\"}}"); } + [Test] + public void TypeId_DictionaryValue_Null_Serialized() + { + var obj = new Dictionary { { "Key", null } }; + + var json = JsonSerializer.Serialize(obj, _options); + + json.Should().Be("{\"Key\":null}"); + } + [Test] public void TypeId_DictionaryKey_DeSerialized() { - var obj = JsonSerializer.Deserialize>($"{{\"{TypeIdStr}\":\"Test\"}}", _options); + var obj = JsonSerializer.Deserialize>($"{{\"{TypeIdStr}\":\"Test\"}}", + _options); + + obj.Should().BeEquivalentTo(new Dictionary + { { TypeId.Parse(TypeIdStr).Decode(), "Test" } }); + } + + [Test] + public void TypeId_DictionaryKey_Null_DeSerialized() + { + // Deserializing a dictionary with a null key is invalid in JSON; test for exception + Action act = () => JsonSerializer.Deserialize>("{null:\"Test\"}", _options); + + act.Should().Throw(); + } + + [Test] + public void TypeId_DictionaryValue_Null_DeSerialized() + { + var obj = JsonSerializer.Deserialize>("{\"Key\":null}", _options); - obj.Should().BeEquivalentTo(new Dictionary { { TypeId.Parse(TypeIdStr).Decode(), "Test" } }); + obj.Should().BeEquivalentTo(new Dictionary { { "Key", null } }); } - private record TypeIdDecodedContainer(TypeIdDecoded Id, int Value); + private record TypeIdDecodedContainer(TypeIdDecoded? Id, int Value); - private record TypeIdDecodedArrayContainer(TypeIdDecoded[] Items); + private record TypeIdDecodedArrayContainer(TypeIdDecoded?[] Items); } \ No newline at end of file From 902aeedea58bf768fbef6aae391f021682a9b4f2 Mon Sep 17 00:00:00 2001 From: Rasmus Soeborg Date: Mon, 16 Sep 2024 11:57:30 +0200 Subject: [PATCH 3/6] test: Introduce nullable handling tests in TypeIdSerialization - Updated and added tests to ensure correct serialization and deserialization of nullable TypeIdDecoded properties within arrays and plain objects. --- .../TypeIdSerializationTests.cs | 95 +++++++++++++++++-- 1 file changed, 89 insertions(+), 6 deletions(-) diff --git a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson.Tests/TypeIdSerializationTests.cs b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson.Tests/TypeIdSerializationTests.cs index ffb4aed..c8283da 100644 --- a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson.Tests/TypeIdSerializationTests.cs +++ b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson.Tests/TypeIdSerializationTests.cs @@ -17,6 +17,15 @@ public void TypeId_Plain_Serialized() json.Should().Be($"\"{TypeIdStr}\""); } + [Test] + public void TypeId_Plain_Null_Serialized() + { + TypeId? typeId = null; + var json = JsonSerializer.Serialize(typeId, _options); + + json.Should().Be("null"); + } + [Test] public void TypeId_NestedProperty_Serialized() { @@ -26,15 +35,35 @@ public void TypeId_NestedProperty_Serialized() json.Should().Be($"{{\"Id\":\"{TypeIdStr}\",\"Value\":42}}"); } + [Test] + public void TypeId_NestedProperty_Null_Serialized() + { + var obj = new TypeIdContainer(null, 42); + var json = JsonSerializer.Serialize(obj, _options); + + json.Should().Be($"{{\"Id\":null,\"Value\":42}}"); + } + [Test] public void TypeId_Collection_Serialized() { - var obj = new TypeIdArrayContainer(new[] { TypeId.Parse(TypeIdStr), TypeId.Parse("prefix_0123456789abcdefghjkmnpqrs") }); + var obj = new TypeIdArrayContainer(new TypeId?[] + { TypeId.Parse(TypeIdStr), TypeId.Parse("prefix_0123456789abcdefghjkmnpqrs") }); var json = JsonSerializer.Serialize(obj, _options); json.Should().Be($"{{\"Items\":[\"{TypeIdStr}\",\"prefix_0123456789abcdefghjkmnpqrs\"]}}"); } + [Test] + public void TypeId_Collection_WithNull_Serialized() + { + var obj = new TypeIdArrayContainer(new TypeId?[] + { TypeId.Parse(TypeIdStr), null }); + var json = JsonSerializer.Serialize(obj, _options); + + json.Should().Be($"{{\"Items\":[\"{TypeIdStr}\",null]}}"); + } + [Test] public void TypeId_Plain_Deserialized() { @@ -43,6 +72,14 @@ public void TypeId_Plain_Deserialized() typeId.Should().Be(TypeId.Parse(TypeIdStr)); } + [Test] + public void TypeId_Plain_Null_Deserialized() + { + var typeId = JsonSerializer.Deserialize("null", _options); + + typeId.Should().BeNull(); + } + [Test] public void TypeId_NestedProperty_Deserialized() { @@ -51,23 +88,53 @@ public void TypeId_NestedProperty_Deserialized() obj.Should().Be(new TypeIdContainer(TypeId.Parse(TypeIdStr), 42)); } + [Test] + public void TypeId_NestedProperty_Null_Deserialized() + { + var obj = JsonSerializer.Deserialize($"{{\"Id\":null,\"Value\":42}}", _options); + + obj.Should().Be(new TypeIdContainer(null, 42)); + } + [Test] public void TypeId_Collection_Deserialized() { - var obj = JsonSerializer.Deserialize($"{{\"Items\":[\"{TypeIdStr}\",\"prefix_0123456789abcdefghjkmnpqrs\"]}}", _options); + var obj = JsonSerializer.Deserialize( + $"{{\"Items\":[\"{TypeIdStr}\",\"prefix_0123456789abcdefghjkmnpqrs\"]}}", _options); + + obj.Should().BeEquivalentTo(new TypeIdArrayContainer(new TypeId?[] + { TypeId.Parse(TypeIdStr), TypeId.Parse("prefix_0123456789abcdefghjkmnpqrs") })); + } + + [Test] + public void TypeId_Collection_WithNull_Deserialized() + { + var obj = JsonSerializer.Deserialize( + $"{{\"Items\":[\"{TypeIdStr}\",null]}}", _options); - obj.Should().BeEquivalentTo(new TypeIdArrayContainer(new[] { TypeId.Parse(TypeIdStr), TypeId.Parse("prefix_0123456789abcdefghjkmnpqrs") })); + obj.Should().BeEquivalentTo(new TypeIdArrayContainer(new TypeId?[] + { TypeId.Parse(TypeIdStr), null })); } [Test] public void TypeId_DictionaryKey_Serialized() { - var obj = new Dictionary { { TypeId.Parse(TypeIdStr), "Test"} }; + var obj = new Dictionary { { TypeId.Parse(TypeIdStr), "Test" } }; var json = JsonSerializer.Serialize(obj, _options); json.Should().Be($"{{\"{TypeIdStr}\":\"Test\"}}"); } + + [Test] + public void TypeId_DictionaryValue_Null_Serialized() + { + var obj = new Dictionary { { "Key", null } }; + + var json = JsonSerializer.Serialize(obj, _options); + + json.Should().Be("{\"Key\":null}"); + } [Test] public void TypeId_DictionaryKey_DeSerialized() @@ -77,7 +144,23 @@ public void TypeId_DictionaryKey_DeSerialized() obj.Should().BeEquivalentTo(new Dictionary { { TypeId.Parse(TypeIdStr), "Test" } }); } - private record TypeIdContainer(TypeId Id, int Value); + [Test] + public void TypeId_DictionaryKey_Null_DeSerialized() + { + Action act = () => JsonSerializer.Deserialize>($"{{null:\"Test\"}}", _options); + + act.Should().Throw(); + } + + [Test] + public void TypeId_DictionaryValue_Null_DeSerialized() + { + var obj = JsonSerializer.Deserialize>("{\"Key\":null}", _options); + + obj.Should().BeEquivalentTo(new Dictionary { { "Key", null } }); + } + + private record TypeIdContainer(TypeId? Id, int Value); - private record TypeIdArrayContainer(TypeId[] Items); + private record TypeIdArrayContainer(TypeId?[] Items); } \ No newline at end of file From 100e7c6122426761c6f6857eeccf209edcaad951 Mon Sep 17 00:00:00 2001 From: Rasmus Soeborg Date: Mon, 16 Sep 2024 11:57:35 +0200 Subject: [PATCH 4/6] refactor: update converter factory references and error handling Replaced direct converter additions in `Extensions.cs` with factory instances. Improved exception messages with more informative details in `TypeIdConverterFactory.cs` and `TypeIdDecodedConverterFactory.cs`. This enhances code maintainability and readability by leveraging factory patterns and explicit error conditions. --- .../Extensions.cs | 4 +- .../TypeIdConverterFactory.cs | 2 +- .../TypeIdDecodedConverterFactory.cs | 37 ++++++++----------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/Extensions.cs b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/Extensions.cs index 5c0dc4f..3e24b1e 100644 --- a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/Extensions.cs +++ b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/Extensions.cs @@ -6,8 +6,8 @@ public static class Extensions { public static JsonSerializerOptions ConfigureForTypeId(this JsonSerializerOptions options) { - options.Converters.Add(new TypeIdConverter()); - options.Converters.Add(new TypeIdDecodedConverter()); + options.Converters.Add(new TypeIdConverterFactory()); + options.Converters.Add(new TypeIdDecodedConverterFactory()); return options; } } \ No newline at end of file diff --git a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdConverterFactory.cs b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdConverterFactory.cs index f331e65..28050ba 100644 --- a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdConverterFactory.cs +++ b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdConverterFactory.cs @@ -20,7 +20,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer BindingFlags.Instance | BindingFlags.Public, null, null, - null) ?? throw new Exception("Could not create converter"); + null) ?? throw new ArgumentException($"Could not create converter of type {typeToConvert}"); } [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", diff --git a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverterFactory.cs b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverterFactory.cs index a6adc21..da29da2 100644 --- a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverterFactory.cs +++ b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverterFactory.cs @@ -1,4 +1,5 @@ -using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; @@ -14,32 +15,28 @@ public override bool CanConvert(Type typeToConvert) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { // Create a converter for the specific type - return (JsonConverter)Activator.CreateInstance( + return (JsonConverter?)Activator.CreateInstance( typeof(TypeIdDecodedConverter<>).MakeGenericType(typeToConvert), BindingFlags.Instance | BindingFlags.Public, - binder: null, - args: null, - culture: null); + null, + null, + null) ?? throw new ArgumentException($"Could not create converter of type {typeToConvert}"); } - private class TypeIdDecodedConverter : JsonConverter + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", + Justification = "reflection")] + private sealed class TypeIdDecodedConverter : JsonConverter { // Determine if the type is nullable private static readonly bool IsNullable = Nullable.GetUnderlyingType(typeof(T)) != null; - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Handle null values if (reader.TokenType == JsonTokenType.Null) { - if (IsNullable) - { - return default; - } - else - { - throw new JsonException($"Cannot convert null to {typeof(T)}."); - } + if (IsNullable) return default; + throw new JsonException($"Cannot convert null to {typeof(T)}."); } var val = reader.GetString(); @@ -71,17 +68,15 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions writer.WriteStringValue(buffer); } - public override T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) { - return Read(ref reader, typeToConvert, options); + return Read(ref reader, typeToConvert, options) ?? throw new JsonException($"Expected a non-null, non-empty string for property name of {typeof(T)}."); } public override void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { - if (value == null) - { - throw new JsonException($"Cannot write null as a property name for {typeof(T)}."); - } + if (value == null) throw new JsonException($"Cannot write null as a property name for {typeof(T)}."); var typedValue = (TypeIdDecoded)(object)value; From feb20999e3858a9985e2a35670ef08bd96d1d0ae Mon Sep 17 00:00:00 2001 From: Rasmus Soeborg Date: Mon, 16 Sep 2024 13:08:56 +0200 Subject: [PATCH 5/6] refactor: update project configuration and improve JSON conversion handling for TypeId --- ...TypeId.Serialization.SystemTextJson.csproj | 14 ++---- .../TypeIdDecodedConverterFactory.cs | 49 ++++++++++++++++--- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeId.Serialization.SystemTextJson.csproj b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeId.Serialization.SystemTextJson.csproj index 903fa7b..38bd8f2 100644 --- a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeId.Serialization.SystemTextJson.csproj +++ b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeId.Serialization.SystemTextJson.csproj @@ -5,12 +5,13 @@ enable enable FastIDs.TypeId.Serialization.SystemTextJson + 1.0.2 - FastIDs.TypeId.Serialization.SystemTextJson + Sublime.FastIDs.TypeId.Serialization.SystemTextJson System.Text.Json serialization helpers for FastIDs.TypeId Apache-2.0 guid,uuid,id,typeid,type-id,uuid7,identifiers,json,jsonnet,serialization - https://github.com/firenero/TypeId + https://github.com/Sublime-IT/TypeId README.md Mykhailo Matviiv true @@ -34,16 +35,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - typeid-textjson-v - true - - diff --git a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverterFactory.cs b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverterFactory.cs index da29da2..cdd3ca0 100644 --- a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverterFactory.cs +++ b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeIdDecodedConverterFactory.cs @@ -39,14 +39,51 @@ private sealed class TypeIdDecodedConverter : JsonConverter throw new JsonException($"Cannot convert null to {typeof(T)}."); } - var val = reader.GetString(); - if (!string.IsNullOrEmpty(val)) + if (reader.TokenType == JsonTokenType.String) { - var decoded = TypeId.Parse(val).Decode(); - return (T)(object)decoded; + var val = reader.GetString(); + if (!string.IsNullOrEmpty(val)) + { + var decoded = TypeId.Parse(val).Decode(); + return (T)(object)decoded; + } + + throw new JsonException($"Expected a non-null, non-empty string for {typeof(T)}."); + } + else if (reader.TokenType == JsonTokenType.StartObject) + { + // Deserialize the object representation + var jsonObject = JsonSerializer.Deserialize(ref reader, options); + + if (jsonObject.TryGetProperty("type", out var typeProperty) && + jsonObject.TryGetProperty("id", out var idProperty)) + { + var type = typeProperty.GetString(); + var id = idProperty.GetString(); + + if (!string.IsNullOrEmpty(type) && !string.IsNullOrEmpty(id)) + { + if (!Guid.TryParse(id, out var parsedId)) + throw new JsonException($"The 'id' property must be a valid UUID for {typeof(T)}."); + + // Create the TypeIdDecoded instance from type and id + var typeId = TypeId.FromUuidV7(type, parsedId); + return (T)(object)typeId; + } + else + { + throw new JsonException($"The 'type' and 'id' properties must be non-null strings for {typeof(T)}."); + } + } + else + { + throw new JsonException($"Expected properties 'type' and 'id' in the JSON object for {typeof(T)}."); + } + } + else + { + throw new JsonException($"Unexpected token parsing {typeof(T)}. Expected String or StartObject, got {reader.TokenType}."); } - - throw new JsonException($"Expected a non-null, non-empty string for {typeof(T)}."); } public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) From 0a0e2d93dd2def8f04b0dcc8ce289161b72dec0c Mon Sep 17 00:00:00 2001 From: Rasmus Soeborg Date: Mon, 16 Sep 2024 13:14:24 +0200 Subject: [PATCH 6/6] revert: changes to SystemTextJson.csproj --- ...TypeId.Serialization.SystemTextJson.csproj | 76 ++++++++++--------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeId.Serialization.SystemTextJson.csproj b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeId.Serialization.SystemTextJson.csproj index 38bd8f2..33617f5 100644 --- a/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeId.Serialization.SystemTextJson.csproj +++ b/src/FastIDs.TypeId.Serialization/TypeId.Serialization.SystemTextJson/TypeId.Serialization.SystemTextJson.csproj @@ -1,41 +1,49 @@ - - net8.0 - enable - enable - FastIDs.TypeId.Serialization.SystemTextJson - 1.0.2 + + net8.0 + enable + enable + FastIDs.TypeId.Serialization.SystemTextJson - Sublime.FastIDs.TypeId.Serialization.SystemTextJson - System.Text.Json serialization helpers for FastIDs.TypeId - Apache-2.0 - guid,uuid,id,typeid,type-id,uuid7,identifiers,json,jsonnet,serialization - https://github.com/Sublime-IT/TypeId - README.md - Mykhailo Matviiv - true - true - snupkg - Copyright (c) Mykhailo Matviiv 2023. - + FastIDs.TypeId.Serialization.SystemTextJson + System.Text.Json serialization helpers for FastIDs.TypeId + Apache-2.0 + guid,uuid,id,typeid,type-id,uuid7,identifiers,json,jsonnet,serialization + https://github.com/firenero/TypeId + README.md + Mykhailo Matviiv + true + true + snupkg + Copyright (c) Mykhailo Matviiv 2023. + - - - + + + - - All - true - + + All + true + - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - + + typeid-textjson-v + true + + + \ No newline at end of file