Skip to content

Commit 57262b1

Browse files
committed
fix STJ issue with nullable smart enums/value objects
#19
1 parent 2a85304 commit 57262b1

File tree

12 files changed

+721
-7
lines changed

12 files changed

+721
-7
lines changed

.serena/project.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,17 @@ read_only_memory_patterns: []
134134
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
135135
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
136136
line_ending:
137+
138+
# list of regex patterns for memories to completely ignore.
139+
# Matching memories will not appear in list_memories or activate_project output
140+
# and cannot be accessed via read_memory or write_memory.
141+
# To access ignored memory files, use the read_file tool on the raw file path.
142+
# Extends the list from the global configuration, merging the two lists.
143+
# Example: ["_archive/.*", "_episodes/.*"]
144+
ignored_memory_patterns: []
145+
146+
# advanced configuration option allowing to configure language server-specific options.
147+
# Maps the language key to the options.
148+
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
149+
# No documentation on options means no options are available.
150+
ls_specific_settings: {}

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<Copyright>(c) $([System.DateTime]::Now.Year), Pawel Gerr. All rights reserved.</Copyright>
5-
<VersionPrefix>10.1.0</VersionPrefix>
5+
<VersionPrefix>10.1.1</VersionPrefix>
66
<Authors>Pawel Gerr</Authors>
77
<GenerateDocumentationFile>true</GenerateDocumentationFile>
88
<PackageProjectUrl>https://github.com/PawelGerr/Thinktecture.Runtime.Extensions</PackageProjectUrl>

src/Thinktecture.Runtime.Extensions.Json/Text/Json/Serialization/ThinktectureJsonConverter.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ public ThinktectureJsonConverter(JsonSerializerOptions options)
3636
/// <inheritdoc />
3737
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
3838
{
39+
if (reader.TokenType == JsonTokenType.Null && default(T) is null)
40+
{
41+
if (_disallowDefaultValues)
42+
throw new JsonException($"Cannot convert null to type \"{typeof(T).Name}\" because it doesn't allow default values.");
43+
44+
return default;
45+
}
46+
3947
var key = _keyConverter is null
4048
? JsonSerializer.Deserialize<TKey>(ref reader, options)
4149
: _keyConverter.Read(ref reader, typeof(TKey), options);
@@ -57,10 +65,13 @@ public ThinktectureJsonConverter(JsonSerializerOptions options)
5765
}
5866

5967
/// <inheritdoc />
60-
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
68+
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
6169
{
6270
if (value is null)
63-
throw new ArgumentNullException(nameof(value));
71+
{
72+
writer.WriteNullValue();
73+
return;
74+
}
6475

6576
if (_keyConverter is null)
6677
{
@@ -116,10 +127,13 @@ public ThinktectureJsonConverter(JsonSerializerOptions options)
116127
}
117128

118129
/// <inheritdoc />
119-
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
130+
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
120131
{
121132
if (value is null)
122-
throw new ArgumentNullException(nameof(value));
133+
{
134+
writer.WriteNullValue();
135+
return;
136+
}
123137

124138
writer.WriteStringValue(value.ToValue());
125139
}

src/Thinktecture.Runtime.Extensions.Json/Text/Json/Serialization/ThinktectureSpanParsableJsonConverter.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,13 @@ public ThinktectureSpanParsableJsonConverter(JsonSerializerOptions options)
4747
}
4848

4949
/// <inheritdoc />
50-
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
50+
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
5151
{
5252
if (value is null)
53-
throw new ArgumentNullException(nameof(value));
53+
{
54+
writer.WriteNullValue();
55+
return;
56+
}
5457

5558
writer.WriteStringValue(value.ToValue());
5659
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
using Thinktecture.Runtime.Tests.TestValueObjects;
4+
5+
namespace Thinktecture.Runtime.Tests.Text.Json.Serialization.ThinktectureJsonConverterFactoryTests;
6+
7+
/// <summary>
8+
/// Tests for ComplexValueObject with nullable keyed value object properties.
9+
/// Related to issue #19: the same root cause (null passed to converter.Write) affects
10+
/// nullable keyed value object properties, not just nullable Smart Enums.
11+
/// </summary>
12+
public class NullableKeyedPropertyTests : JsonTestsBase
13+
{
14+
[Fact]
15+
public void Should_serialize_complex_value_object_with_null_string_based_keyed_value_object_property()
16+
{
17+
var obj = ComplexValueObjectWithNullableKeyedProperties.Create(null, null, null, 42);
18+
19+
var json = Serialize(obj);
20+
21+
json.Should().Be("{\"NullableStringBasedValueObject\":null,\"NullableIntBasedValueObject\":null,\"NullableIntBasedStructValueObject\":null,\"OtherProperty\":42}");
22+
}
23+
24+
[Fact]
25+
public void Should_roundtrip_complex_value_object_with_null_keyed_value_object_properties()
26+
{
27+
var obj = ComplexValueObjectWithNullableKeyedProperties.Create(null, null, null, 42);
28+
29+
var json = Serialize(obj);
30+
var deserialized = Deserialize<ComplexValueObjectWithNullableKeyedProperties>(json);
31+
32+
deserialized.Should().Be(obj);
33+
}
34+
35+
[Fact]
36+
public void Should_serialize_complex_value_object_with_partial_null_keyed_properties()
37+
{
38+
var obj = ComplexValueObjectWithNullableKeyedProperties.Create(
39+
StringBasedReferenceValueObject.Create("test"),
40+
null,
41+
null,
42+
42);
43+
44+
var json = Serialize(obj);
45+
46+
json.Should().Be("{\"NullableStringBasedValueObject\":\"test\",\"NullableIntBasedValueObject\":null,\"NullableIntBasedStructValueObject\":null,\"OtherProperty\":42}");
47+
}
48+
49+
// ── Regression tests (expected to pass) ──
50+
51+
[Fact]
52+
public void Should_serialize_complex_value_object_with_non_null_keyed_value_object_properties()
53+
{
54+
var obj = ComplexValueObjectWithNullableKeyedProperties.Create(
55+
StringBasedReferenceValueObject.Create("test"),
56+
IntBasedReferenceValueObject.Create(1),
57+
IntBasedStructValueObject.Create(2),
58+
42);
59+
60+
var json = Serialize(obj);
61+
62+
json.Should().Be("{\"NullableStringBasedValueObject\":\"test\",\"NullableIntBasedValueObject\":1,\"NullableIntBasedStructValueObject\":2,\"OtherProperty\":42}");
63+
}
64+
65+
[Fact]
66+
public void Should_roundtrip_complex_value_object_with_non_null_keyed_value_object_properties()
67+
{
68+
var obj = ComplexValueObjectWithNullableKeyedProperties.Create(
69+
StringBasedReferenceValueObject.Create("test"),
70+
IntBasedReferenceValueObject.Create(1),
71+
IntBasedStructValueObject.Create(2),
72+
42);
73+
74+
var json = Serialize(obj);
75+
var deserialized = Deserialize<ComplexValueObjectWithNullableKeyedProperties>(json);
76+
77+
deserialized.Should().Be(obj);
78+
}
79+
80+
[Fact]
81+
public void Should_skip_null_keyed_properties_when_ignore_condition_is_WhenWritingNull()
82+
{
83+
var obj = ComplexValueObjectWithNullableKeyedProperties.Create(null, null, null, 42);
84+
85+
var json = Serialize(obj, null, JsonIgnoreCondition.WhenWritingNull);
86+
87+
json.Should().Be("{\"OtherProperty\":42}");
88+
}
89+
90+
[Fact]
91+
public void Should_skip_null_keyed_properties_when_ignore_condition_is_WhenWritingDefault()
92+
{
93+
var obj = ComplexValueObjectWithNullableKeyedProperties.Create(null, null, null, 42);
94+
95+
var json = Serialize(obj, null, JsonIgnoreCondition.WhenWritingDefault);
96+
97+
json.Should().Be("{\"OtherProperty\":42}");
98+
}
99+
100+
[Fact]
101+
public void Should_deserialize_complex_value_object_with_null_keyed_value_object_properties_from_json()
102+
{
103+
var json = "{\"NullableStringBasedValueObject\":null,\"NullableIntBasedValueObject\":null,\"NullableIntBasedStructValueObject\":null,\"OtherProperty\":42}";
104+
105+
var deserialized = Deserialize<ComplexValueObjectWithNullableKeyedProperties>(json);
106+
107+
deserialized.NullableStringBasedValueObject.Should().BeNull();
108+
deserialized.NullableIntBasedValueObject.Should().BeNull();
109+
deserialized.NullableIntBasedStructValueObject.Should().BeNull();
110+
deserialized.OtherProperty.Should().Be(42);
111+
}
112+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
using Thinktecture.Runtime.Tests.TestEnums;
4+
using Thinktecture.Runtime.Tests.TestValueObjects;
5+
6+
namespace Thinktecture.Runtime.Tests.Text.Json.Serialization.ThinktectureJsonConverterFactoryTests;
7+
8+
// ReSharper disable once InconsistentNaming
9+
/// <summary>
10+
/// Tests for issue #19: ComplexValueObject with nullable SmartEnum property throws
11+
/// ArgumentNullException during JSON serialization when the property value is null.
12+
/// </summary>
13+
public class NullableSmartEnumPropertyTests : JsonTestsBase
14+
{
15+
[Fact]
16+
public void Should_serialize_complex_value_object_with_null_string_based_smart_enum_property()
17+
{
18+
var obj = ComplexValueObjectWithNullableSmartEnumProperty.Create(null, null, 42);
19+
20+
var json = Serialize(obj);
21+
22+
json.Should().Be("{\"NullableStringBasedSmartEnum\":null,\"NullableIntBasedSmartEnum\":null,\"OtherProperty\":42}");
23+
}
24+
25+
[Fact]
26+
public void Should_roundtrip_complex_value_object_with_null_smart_enum_properties()
27+
{
28+
var obj = ComplexValueObjectWithNullableSmartEnumProperty.Create(null, null, 42);
29+
30+
var json = Serialize(obj);
31+
var deserialized = Deserialize<ComplexValueObjectWithNullableSmartEnumProperty>(json);
32+
33+
deserialized.Should().Be(obj);
34+
}
35+
36+
[Fact]
37+
public void Should_serialize_complex_value_object_with_null_string_based_smart_enum_and_non_null_int_based()
38+
{
39+
var obj = ComplexValueObjectWithNullableSmartEnumProperty.Create(null, SmartEnum_IntBased.Item1, 42);
40+
41+
var json = Serialize(obj);
42+
43+
json.Should().Be("{\"NullableStringBasedSmartEnum\":null,\"NullableIntBasedSmartEnum\":1,\"OtherProperty\":42}");
44+
}
45+
46+
[Fact]
47+
public void Should_serialize_complex_value_object_with_non_null_string_based_and_null_int_based_smart_enum()
48+
{
49+
var obj = ComplexValueObjectWithNullableSmartEnumProperty.Create(SmartEnum_StringBased.Item1, null, 42);
50+
51+
var json = Serialize(obj);
52+
53+
json.Should().Be("{\"NullableStringBasedSmartEnum\":\"Item1\",\"NullableIntBasedSmartEnum\":null,\"OtherProperty\":42}");
54+
}
55+
56+
// ── Regression tests (expected to pass) ──
57+
58+
[Fact]
59+
public void Should_serialize_complex_value_object_with_non_null_smart_enum_properties()
60+
{
61+
var obj = ComplexValueObjectWithNullableSmartEnumProperty.Create(SmartEnum_StringBased.Item1, SmartEnum_IntBased.Item2, 42);
62+
63+
var json = Serialize(obj);
64+
65+
json.Should().Be("{\"NullableStringBasedSmartEnum\":\"Item1\",\"NullableIntBasedSmartEnum\":2,\"OtherProperty\":42}");
66+
}
67+
68+
[Fact]
69+
public void Should_roundtrip_complex_value_object_with_non_null_smart_enum_properties()
70+
{
71+
var obj = ComplexValueObjectWithNullableSmartEnumProperty.Create(SmartEnum_StringBased.Item1, SmartEnum_IntBased.Item2, 42);
72+
73+
var json = Serialize(obj);
74+
var deserialized = Deserialize<ComplexValueObjectWithNullableSmartEnumProperty>(json);
75+
76+
deserialized.Should().Be(obj);
77+
}
78+
79+
[Fact]
80+
public void Should_skip_null_smart_enum_properties_when_ignore_condition_is_WhenWritingNull()
81+
{
82+
var obj = ComplexValueObjectWithNullableSmartEnumProperty.Create(null, null, 42);
83+
84+
var json = Serialize(obj, null, JsonIgnoreCondition.WhenWritingNull);
85+
86+
json.Should().Be("{\"OtherProperty\":42}");
87+
}
88+
89+
[Fact]
90+
public void Should_skip_null_smart_enum_properties_when_ignore_condition_is_WhenWritingDefault()
91+
{
92+
var obj = ComplexValueObjectWithNullableSmartEnumProperty.Create(null, null, 42);
93+
94+
var json = Serialize(obj, null, JsonIgnoreCondition.WhenWritingDefault);
95+
96+
json.Should().Be("{\"OtherProperty\":42}");
97+
}
98+
99+
[Fact]
100+
public void Should_serialize_null_complex_value_object_with_nullable_smart_enums_as_null()
101+
{
102+
var json = Serialize<ComplexValueObjectWithNullableSmartEnumProperty>(null);
103+
104+
json.Should().Be("null");
105+
}
106+
107+
[Fact]
108+
public void Should_deserialize_complex_value_object_with_null_smart_enum_properties_from_json()
109+
{
110+
var json = "{\"NullableStringBasedSmartEnum\":null,\"NullableIntBasedSmartEnum\":null,\"OtherProperty\":42}";
111+
112+
var deserialized = Deserialize<ComplexValueObjectWithNullableSmartEnumProperty>(json);
113+
114+
deserialized.NullableStringBasedSmartEnum.Should().BeNull();
115+
deserialized.NullableIntBasedSmartEnum.Should().BeNull();
116+
deserialized.OtherProperty.Should().Be(42);
117+
}
118+
119+
[Fact]
120+
public void Should_serialize_with_camel_case_naming_policy()
121+
{
122+
var obj = ComplexValueObjectWithNullableSmartEnumProperty.Create(null, null, 42);
123+
124+
var json = Serialize(obj, JsonNamingPolicy.CamelCase);
125+
126+
json.Should().Be("{\"nullableStringBasedSmartEnum\":null,\"nullableIntBasedSmartEnum\":null,\"otherProperty\":42}");
127+
}
128+
}

0 commit comments

Comments
 (0)