Skip to content

Commit cbd28fa

Browse files
committed
Adding a general management of json errors for some types
1 parent f0480a1 commit cbd28fa

File tree

8 files changed

+312
-42
lines changed

8 files changed

+312
-42
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
using System.Globalization;
2+
using NetDaemon.Client.Internal.Json;
3+
4+
namespace NetDaemon.HassClient.Tests.Json;
5+
6+
public class EnsureExpectedDatatypeConverterTest
7+
{
8+
private readonly JsonSerializerOptions _defaultSerializerOptions = DefaultSerializerOptions.DeserializationOptions;
9+
10+
[Fact]
11+
public void TestConvertAllSupportedTypesConvertsCorrectly()
12+
{
13+
var json = @"
14+
{
15+
""string"": ""string"",
16+
""int"": 10000,
17+
""short"": 2000,
18+
""float"": 1.1,
19+
""bool"": true,
20+
""datetime"": ""2019-02-16T18:11:44.183673+00:00""
21+
}
22+
";
23+
24+
var record = JsonSerializer.Deserialize<SupportedTypesTestRecord>(json, _defaultSerializerOptions);
25+
record!.SomeString.Should().Be("string");
26+
record!.SomeInt.Should().Be(10000);
27+
record!.SomeShort.Should().Be(2000);
28+
record!.SomeFloat.Should().Be(1.1f);
29+
record!.SomeBool.Should().BeTrue();
30+
record!.SomeDateTime.Should().Be(DateTime.Parse("2019-02-16T18:11:44.183673+00:00", CultureInfo.InvariantCulture));
31+
}
32+
33+
[Fact]
34+
public void TestConvertAllSupportedTypesConvertsToNullWhenWrongDatatypeCorrectly()
35+
{
36+
var json = @"
37+
{
38+
""string"": 1,
39+
""int"": ""10000"",
40+
""short"": ""2000"",
41+
""float"": {""property"": ""100""},
42+
""bool"": ""hello"",
43+
""datetime"": ""test""
44+
}
45+
";
46+
47+
var record = JsonSerializer.Deserialize<SupportedTypesTestRecord>(json, _defaultSerializerOptions);
48+
record!.SomeString.Should().BeNull();
49+
record!.SomeInt.Should().BeNull();
50+
record!.SomeShort.Should().BeNull();
51+
record!.SomeFloat.Should().BeNull();
52+
record!.SomeBool.Should().BeNull();
53+
record!.SomeDateTime.Should().BeNull();
54+
}
55+
56+
[Fact]
57+
public void TestConvertAllSupportedTypesConvertsToNullWhenNullJsonCorrectly()
58+
{
59+
var json = @"
60+
{
61+
""string"": null,
62+
""int"": null,
63+
""short"": null,
64+
""float"": null,
65+
""bool"": null,
66+
""datetime"": null
67+
}
68+
";
69+
70+
var record = JsonSerializer.Deserialize<SupportedTypesTestRecord>(json, _defaultSerializerOptions);
71+
record!.SomeString.Should().BeNull();
72+
record!.SomeInt.Should().BeNull();
73+
record!.SomeShort.Should().BeNull();
74+
record!.SomeFloat.Should().BeNull();
75+
record!.SomeBool.Should().BeNull();
76+
record!.SomeDateTime.Should().BeNull();
77+
}
78+
79+
[Fact]
80+
public void TestConvertAllNonNullShouldThrowExcptionIfThereAreADatatypeError()
81+
{
82+
var json = @"
83+
{
84+
""string"": 1
85+
}
86+
";
87+
var result = JsonSerializer.Deserialize<SupportedTypesNonNullTestRecord>(json, _defaultSerializerOptions);
88+
// The string can be null even if not nullable so it will not throw.
89+
result!.SomeString.Should().BeNull();
90+
json = @"
91+
{
92+
""int"": ""10000""
93+
}
94+
";
95+
FluentActions.Invoking(() => JsonSerializer.Deserialize<SupportedTypesNonNullTestRecord>(json, _defaultSerializerOptions))
96+
.Should().Throw<JsonException>();
97+
json = @"
98+
{
99+
""short"": ""2000""
100+
}
101+
";
102+
FluentActions.Invoking(() => JsonSerializer.Deserialize<SupportedTypesNonNullTestRecord>(json, _defaultSerializerOptions))
103+
.Should().Throw<JsonException>();
104+
json = @"
105+
{
106+
""float"": {""property"": ""100""}
107+
}
108+
";
109+
FluentActions.Invoking(() => JsonSerializer.Deserialize<SupportedTypesNonNullTestRecord>(json, _defaultSerializerOptions))
110+
.Should().Throw<JsonException>();
111+
json = @"
112+
{
113+
""bool"": ""hello""
114+
}
115+
";
116+
FluentActions.Invoking(() => JsonSerializer.Deserialize<SupportedTypesNonNullTestRecord>(json, _defaultSerializerOptions))
117+
.Should().Throw<JsonException>();
118+
json = @"
119+
{
120+
""datetime"": ""test""
121+
}
122+
";
123+
FluentActions.Invoking(() => JsonSerializer.Deserialize<SupportedTypesNonNullTestRecord>(json, _defaultSerializerOptions))
124+
.Should().Throw<JsonException>();
125+
}
126+
}
127+
128+
public record SupportedTypesTestRecord
129+
{
130+
[JsonPropertyName("string")] public string? SomeString { get; init; }
131+
[JsonPropertyName("int")] public int? SomeInt { get; init; }
132+
[JsonPropertyName("short")] public short? SomeShort { get; init; }
133+
[JsonPropertyName("float")] public float? SomeFloat { get; init; }
134+
[JsonPropertyName("bool")] public bool? SomeBool { get; init; }
135+
[JsonPropertyName("datetime")] public DateTime? SomeDateTime { get; init; }
136+
}
137+
138+
public record SupportedTypesNonNullTestRecord
139+
{
140+
[JsonPropertyName("string")] public string SomeString { get; init; } = string.Empty;
141+
[JsonPropertyName("int")] public int SomeInt { get; init; }
142+
[JsonPropertyName("short")] public short SomeShort { get; init; }
143+
[JsonPropertyName("float")] public float SomeFloat { get; init; }
144+
[JsonPropertyName("bool")] public bool SomeBool { get; init; }
145+
[JsonPropertyName("datetime")] public DateTime SomeDateTime { get; init; }
146+
}

src/Client/NetDaemon.HassClient.Tests/Json/EnsureStringConverterTests.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11

2+
using NetDaemon.Client.Internal.Json;
3+
24
namespace NetDaemon.HassClient.Tests.Json;
35

46
public class EnsureStringConverterTests
57
{
68
/// <summary>
79
/// Default Json serialization options, Hass expects intended
810
/// </summary>
9-
private readonly JsonSerializerOptions _defaultSerializerOptions = new()
10-
{
11-
WriteIndented = false,
12-
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
13-
};
11+
private readonly JsonSerializerOptions _defaultSerializerOptions = DefaultSerializerOptions.DeserializationOptions;
1412

1513
[Fact]
1614
public void TestConvertAValidString()

src/Client/NetDaemon.HassClient/Common/HomeAssistant/Extensions/HassEventExtensions.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace NetDaemon.Client.HomeAssistant.Extensions;
55
/// </summary>
66
public static class HassEventExtensions
77
{
8+
private static readonly JsonSerializerOptions _jsonSerializerOptions = DefaultSerializerOptions.DeserializationOptions;
89
/// <summary>
910
/// Convert a HassEvent to a StateChangedEvent
1011
/// </summary>
@@ -14,7 +15,7 @@ public static class HassEventExtensions
1415
{
1516
var jsonElement = hassEvent.DataElement ??
1617
throw new NullReferenceException("DataElement cannot be empty");
17-
return jsonElement.Deserialize<HassStateChangedEventData>();
18+
return jsonElement.Deserialize<HassStateChangedEventData>(_jsonSerializerOptions);
1819
}
1920

2021
/// <summary>
@@ -26,6 +27,6 @@ public static class HassEventExtensions
2627
{
2728
var jsonElement = hassEvent.DataElement ??
2829
throw new NullReferenceException("DataElement cannot be empty");
29-
return jsonElement.Deserialize<HassServiceEventData>();
30+
return jsonElement.Deserialize<HassServiceEventData>(_jsonSerializerOptions);
3031
}
3132
}

src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassDevice.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ public record HassDevice
2222
#pragma warning disable CA1056 // It's ok for this URL to be a string
2323
[JsonPropertyName("configuration_url")] public string? ConfigurationUrl { get; init; }
2424
#pragma warning restore CA1056
25-
[JsonConverter(typeof(EnsureStringConverter))]
2625
[JsonPropertyName("hw_version")] public string? HardwareVersion { get; init; }
27-
[JsonConverter(typeof(EnsureStringConverter))]
2826
[JsonPropertyName("sw_version")] public string? SoftwareVersion { get; init; }
29-
[JsonConverter(typeof(EnsureStringConverter))]
3027
[JsonPropertyName("serial_number")] public string? SerialNumber { get; init; }
3128
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace NetDaemon.Client.Internal.Json;
2+
3+
//<summary>
4+
// Default options for serialization when serializing and deserializing json
5+
// </summary>
6+
internal static class DefaultSerializerOptions
7+
{
8+
public static JsonSerializerOptions DeserializationOptions => new()
9+
{
10+
Converters =
11+
{
12+
new EnsureStringConverter(),
13+
new EnsureIntConverter(),
14+
new EnsureShortConverter(),
15+
new EnsureBooleanConverter(),
16+
new EnsureFloatConverter(),
17+
new EnsureDateTimeConverter()
18+
}
19+
};
20+
21+
public static JsonSerializerOptions SerializationOptions => new()
22+
{
23+
WriteIndented = false,
24+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
25+
};
26+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
namespace NetDaemon.Client.Internal.Json;
2+
3+
/// <summary>
4+
/// Base class for converters that ensures the expected suported datatyps
5+
/// </summary>
6+
/// <remarks>
7+
/// This is a workaround to make the serializer to not throw exceptions when there are unexpected datatypes returning from Home Assistant json
8+
/// This converter will only be used when deserializing json
9+
///
10+
/// Note: Tried to make a even smarter generic class but could not get it to avoid recursion
11+
/// </remarks>
12+
internal abstract class EnsureExcpectedDatatypeConverterBase<T> : JsonConverter<T?>
13+
{
14+
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
15+
throw new NotImplementedException();
16+
17+
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
18+
{
19+
JsonSerializer.Serialize(writer, value, typeof(T), options);
20+
}
21+
22+
protected static object? ReadTokenSuccessfullyOrNull(ref Utf8JsonReader reader, JsonTokenType[] tokenType)
23+
{
24+
if (!tokenType.Contains(reader.TokenType))
25+
{
26+
// Skip the children of current token if it is not the expected one
27+
reader.Skip();
28+
return null;
29+
}
30+
31+
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
32+
33+
try
34+
{
35+
return Type.GetTypeCode(type) switch
36+
{
37+
TypeCode.String => reader.GetString(),
38+
TypeCode.Int32 => reader.GetInt32(),
39+
TypeCode.Int16 => reader.GetInt16(),
40+
TypeCode.Boolean => reader.GetBoolean(),
41+
TypeCode.Single => reader.GetSingle(),
42+
TypeCode.DateTime => reader.GetDateTime(),
43+
_ => throw new NotImplementedException($"Type {typeof(T)} with timecode {Type.GetTypeCode(type)} is not implemented")
44+
};
45+
}
46+
catch (JsonException)
47+
{
48+
// Skip the children of current token
49+
reader.Skip();
50+
return null;
51+
}
52+
catch (FormatException)
53+
{
54+
// We are getting this exception when for example there are a format error of dates etc
55+
// I am reluctant if this error really should just return null, codereview should discuss
56+
// Maybe trace log the error?
57+
reader.Skip();
58+
return null;
59+
}
60+
}
61+
}
62+
63+
/// <summary>
64+
/// Converts a Json element that can be a string or returns null if it is not a string
65+
/// </summary>
66+
internal class EnsureStringConverter : EnsureExcpectedDatatypeConverterBase<string?>
67+
{
68+
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
69+
ReadTokenSuccessfullyOrNull(ref reader, [JsonTokenType.String, JsonTokenType.Null]) as string;
70+
}
71+
72+
/// <summary>
73+
/// Converts a Json element that can be a int or returns null if it is not a int
74+
/// </summary>
75+
internal class EnsureIntConverter : EnsureExcpectedDatatypeConverterBase<int?>
76+
{
77+
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
78+
(int?) ReadTokenSuccessfullyOrNull(ref reader, [JsonTokenType.Number, JsonTokenType.Null]);
79+
}
80+
81+
/// <summary>
82+
/// Converts a Json element that can be a short or returns null if it is not a short
83+
/// </summary>
84+
internal class EnsureShortConverter : EnsureExcpectedDatatypeConverterBase<short?>
85+
{
86+
public override short? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
87+
(short?) ReadTokenSuccessfullyOrNull(ref reader, [JsonTokenType.Number, JsonTokenType.Null]);
88+
}
89+
90+
/// <summary>
91+
/// Converts a Json element that can be a float or returns null if it is not afloat
92+
/// </summary>
93+
internal class EnsureFloatConverter : EnsureExcpectedDatatypeConverterBase<float?>
94+
{
95+
public override float? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
96+
(float?) ReadTokenSuccessfullyOrNull(ref reader, [JsonTokenType.Number, JsonTokenType.Null]);
97+
}
98+
99+
/// <summary>
100+
/// Converts a Json element that can be a boolean or returns null if it is not a boolean
101+
/// </summary>
102+
internal class EnsureBooleanConverter : EnsureExcpectedDatatypeConverterBase<bool?>
103+
{
104+
public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
105+
(bool?) ReadTokenSuccessfullyOrNull(ref reader, [JsonTokenType.True, JsonTokenType.False, JsonTokenType.Null]);
106+
}
107+
108+
/// <summary>
109+
/// Converts a Json element that can be a string or returns null if it is not a string
110+
/// </summary>
111+
internal class EnsureDateTimeConverter : EnsureExcpectedDatatypeConverterBase<DateTime?>
112+
{
113+
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
114+
(DateTime?) ReadTokenSuccessfullyOrNull(ref reader, [JsonTokenType.String, JsonTokenType.Null]);
115+
}
116+
117+
/// <summary>
118+
/// Return all the converters that should be used when deserializing
119+
/// </summary>
120+
internal static class EnsureExpectedDatatypeConverter
121+
{
122+
public static IList<JsonConverter> Converters() =>
123+
[
124+
new EnsureStringConverter(),
125+
new EnsureIntConverter(),
126+
new EnsureShortConverter(),
127+
new EnsureFloatConverter(),
128+
new EnsureBooleanConverter(),
129+
new EnsureDateTimeConverter()
130+
];
131+
}

src/Client/NetDaemon.HassClient/Internal/Json/EnsureStringConverter.cs

Lines changed: 0 additions & 23 deletions
This file was deleted.

0 commit comments

Comments
 (0)