Skip to content

Commit 25b6558

Browse files
authored
Fix JSON serialization of quantities with decimal values (#868)
Add IDecimalQuantity interface to expose the decimal value Serialize decimal values as string to keep number of decimal places
1 parent c46d064 commit 25b6558

File tree

14 files changed

+302
-76
lines changed

14 files changed

+302
-76
lines changed

CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,15 @@ namespace UnitsNet
6969
/// {_quantity.XmlDocRemarks}
7070
/// </remarks>");
7171

72+
Writer.W(@$"
73+
public partial struct {_quantity.Name} : IQuantity<{_unitEnumName}>, ");
74+
if (_quantity.BaseType == "decimal")
75+
{
76+
Writer.W("IDecimalQuantity, ");
77+
}
78+
79+
Writer.WL($"IEquatable<{_quantity.Name}>, IComparable, IComparable<{_quantity.Name}>, IConvertible, IFormattable");
7280
Writer.WL($@"
73-
public partial struct {_quantity.Name} : IQuantity<{_unitEnumName}>, IEquatable<{_quantity.Name}>, IComparable, IComparable<{_quantity.Name}>, IConvertible, IFormattable
7481
{{
7582
/// <summary>
7683
/// The numeric value this quantity was constructed with.
@@ -269,6 +276,11 @@ private void GenerateProperties()
269276
Writer.WL(@"
270277
double IQuantity.Value => (double) _value;
271278
");
279+
if (_quantity.BaseType == "decimal")
280+
Writer.WL(@"
281+
/// <inheritdoc cref=""IDecimalQuantity.Value""/>
282+
decimal IDecimalQuantity.Value => _value;
283+
");
272284

273285
Writer.WL($@"
274286
Enum IQuantity.Unit => Unit;

CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ public void Ctor_WithUndefinedUnit_ThrowsArgumentException()
110110
public void DefaultCtor_ReturnsQuantityWithZeroValueAndBaseUnit()
111111
{{
112112
var quantity = new {_quantity.Name}();
113-
Assert.Equal(0, quantity.Value);
113+
Assert.Equal(0, quantity.Value);");
114+
if (_quantity.BaseType == "decimal") Writer.WL($@"
115+
Assert.Equal(0m, ((IDecimalQuantity)quantity).Value);");
116+
Writer.WL($@"
114117
Assert.Equal({_baseUnitFullName}, quantity.Unit);
115118
}}
116119

UnitsNet.Serialization.JsonNet.Tests/UnitsNetBaseJsonConverterTest.cs

Lines changed: 112 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Globalization;
67
using Newtonsoft.Json;
78
using Newtonsoft.Json.Converters;
89
using Newtonsoft.Json.Linq;
@@ -13,38 +14,47 @@ namespace UnitsNet.Serialization.JsonNet.Tests
1314
{
1415
public sealed class UnitsNetBaseJsonConverterTest
1516
{
16-
private TestConverter _sut;
17+
private readonly TestConverter _sut;
1718

1819
public UnitsNetBaseJsonConverterTest()
1920
{
2021
_sut = new TestConverter();
2122
}
2223

2324
[Fact]
24-
public void UnitsNetBaseJsonConverter_ConvertIQuantity_works_as_expected()
25+
public void UnitsNetBaseJsonConverter_ConvertIQuantity_works_with_double_type()
2526
{
26-
var result = _sut.Test_ConvertIQuantity(Power.FromWatts(10.2365D));
27+
var result = _sut.Test_ConvertDoubleIQuantity(Length.FromMeters(10.2365));
28+
29+
Assert.Equal("LengthUnit.Meter", result.Unit);
30+
Assert.Equal(10.2365, result.Value);
31+
}
32+
33+
[Fact]
34+
public void UnitsNetBaseJsonConverter_ConvertIQuantity_works_with_decimal_type()
35+
{
36+
var result = _sut.Test_ConvertDecimalIQuantity(Power.FromWatts(10.2365m));
2737

2838
Assert.Equal("PowerUnit.Watt", result.Unit);
29-
Assert.Equal(10.2365D, result.Value);
39+
Assert.Equal(10.2365m, result.Value);
3040
}
3141

3242
[Fact]
3343
public void UnitsNetBaseJsonConverter_ConvertIQuantity_throws_ArgumentNullException_when_quantity_is_NULL()
3444
{
35-
var result = Assert.Throws<ArgumentNullException>(() => _sut.Test_ConvertIQuantity(null));
45+
var result = Assert.Throws<ArgumentNullException>(() => _sut.Test_ConvertDoubleIQuantity(null));
3646

3747
Assert.Equal("Value cannot be null.\r\nParameter name: quantity", result.Message);
3848
}
3949

4050
[Fact]
4151
public void UnitsNetBaseJsonConverter_ConvertValueUnit_works_as_expected()
4252
{
43-
var result = _sut.Test_ConvertValueUnit("PowerUnit.Watt", 10.2365D);
53+
var result = _sut.Test_ConvertDecimalValueUnit("PowerUnit.Watt", 10.2365m);
4454

4555
Assert.NotNull(result);
4656
Assert.IsType<Power>(result);
47-
Assert.True(Power.FromWatts(10.2365D).Equals((Power)result, 1E-5, ComparisonType.Absolute));
57+
Assert.True(Power.FromWatts(10.2365m).Equals((Power)result, 1E-5, ComparisonType.Absolute));
4858

4959
}
5060

@@ -59,7 +69,7 @@ public void UnitsNetBaseJsonConverter_ConvertValueUnit_works_with_NULL_value()
5969
[Fact]
6070
public void UnitsNetBaseJsonConverter_ConvertValueUnit_throws_UnitsNetException_when_unit_does_not_exist()
6171
{
62-
var result = Assert.Throws<UnitsNetException>(() => _sut.Test_ConvertValueUnit("SomeImaginaryUnit.Watt", 10.2365D));
72+
var result = Assert.Throws<UnitsNetException>(() => _sut.Test_ConvertDoubleValueUnit("SomeImaginaryUnit.Watt", 10.2365D));
6373

6474
Assert.Equal("Unable to find enum type.", result.Message);
6575
Assert.True(result.Data.Contains("type"));
@@ -69,7 +79,7 @@ public void UnitsNetBaseJsonConverter_ConvertValueUnit_throws_UnitsNetException_
6979
[Fact]
7080
public void UnitsNetBaseJsonConverter_ConvertValueUnit_throws_UnitsNetException_when_unit_is_in_unexpected_format()
7181
{
72-
var result = Assert.Throws<UnitsNetException>(() => _sut.Test_ConvertValueUnit("PowerUnit Watt", 10.2365D));
82+
var result = Assert.Throws<UnitsNetException>(() => _sut.Test_ConvertDecimalValueUnit("PowerUnit Watt", 10.2365m));
7383

7484
Assert.Equal("\"PowerUnit Watt\" is not a valid unit.", result.Message);
7585
Assert.True(result.Data.Contains("type"));
@@ -85,7 +95,7 @@ public void UnitsNetBaseJsonConverter_CreateLocalSerializer_works_as_expected()
8595
TypeNameHandling = TypeNameHandling.Arrays,
8696
Converters = new List<JsonConverter>()
8797
{
88-
98+
8999
new BinaryConverter(),
90100
_sut,
91101
new DataTableConverter()
@@ -104,26 +114,56 @@ public void UnitsNetBaseJsonConverter_CreateLocalSerializer_works_as_expected()
104114
}
105115

106116
[Fact]
107-
public void UnitsNetBaseJsonConverter_ReadValueUnit_work_as_expected()
117+
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_with_double_quantity()
108118
{
109-
var token = new JObject();
119+
var token = new JObject {{"Unit", "LengthUnit.Meter"}, {"Value", 10.2365}};
110120

111-
token.Add("Unit", "PowerUnit.Watt");
112-
token.Add("Value", 10.2365D);
121+
var result = _sut.Test_ReadDoubleValueUnit(token);
122+
123+
Assert.NotNull(result);
124+
Assert.Equal("LengthUnit.Meter", result?.Unit);
125+
Assert.Equal(10.2365, result?.Value);
126+
}
127+
128+
[Fact]
129+
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_with_decimal_quantity()
130+
{
131+
var token = new JObject {{"Unit", "PowerUnit.Watt"}, {"Value", 10.2365m}, {"ValueString", "10.2365"}, {"ValueType", "decimal"}};
113132

114-
var result = _sut.Test_ReadValueUnit(token);
133+
var result = _sut.Test_ReadDecimalValueUnit(token);
115134

116135
Assert.NotNull(result);
117136
Assert.Equal("PowerUnit.Watt", result?.Unit);
118-
Assert.Equal(10.2365D, result?.Value);
137+
Assert.Equal(10.2365m, result?.Value);
138+
}
139+
140+
[Fact]
141+
public void UnitsNetBaseJsonConverter_ReadValueUnit_returns_null_when_value_is_a_string()
142+
{
143+
var token = new JObject {{"Unit", "PowerUnit.Watt"}, {"Value", "10.2365"}};
144+
145+
var result = _sut.Test_ReadDecimalValueUnit(token);
146+
147+
Assert.Null(result);
148+
}
149+
150+
[Fact]
151+
public void UnitsNetBaseJsonConverter_ReadValueUnit_returns_null_when_value_type_is_not_a_string()
152+
{
153+
var token = new JObject {{"Unit", "PowerUnit.Watt"}, {"Value", 10.2365}, {"ValueType", 123}};
154+
155+
var result = _sut.Test_ReadDecimalValueUnit(token);
156+
157+
Assert.Null(result);
119158
}
120159

160+
121161
[Fact]
122-
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_with_empty_token()
162+
public void UnitsNetBaseJsonConverter_ReadDoubleValueUnit_works_with_empty_token()
123163
{
124164
var token = new JObject();
125165

126-
var result = _sut.Test_ReadValueUnit(token);
166+
var result = _sut.Test_ReadDoubleValueUnit(token);
127167

128168
Assert.Null(result);
129169
}
@@ -142,32 +182,40 @@ public void UnitsNetBaseJsonConverter_ReadValueUnit_returns_null_when_unit_or_va
142182

143183
if (withValue)
144184
{
145-
token.Add("Value", 10.2365D);
185+
token.Add("Value", 10.2365m);
146186
}
147187

148-
var result = _sut.Test_ReadValueUnit(token);
188+
var result = _sut.Test_ReadDecimalValueUnit(token);
149189

150190
Assert.Null(result);
151191
}
152192

153193
[Theory]
154-
[InlineData("Unit", "Value")]
155-
[InlineData("unit", "Value")]
156-
[InlineData("Unit", "value")]
157-
[InlineData("unit", "value")]
158-
[InlineData("unIT", "vAlUe")]
159-
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_case_insensitive(string unitPropertyName, string valuePropertyName)
194+
[InlineData("Unit", "Value", "ValueString", "ValueType")]
195+
[InlineData("unit", "Value", "ValueString", "ValueType")]
196+
[InlineData("Unit", "value", "valueString", "valueType")]
197+
[InlineData("unit", "value", "valueString", "valueType")]
198+
[InlineData("unIT", "vAlUe", "vAlUeString", "vAlUeType")]
199+
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_case_insensitive(
200+
string unitPropertyName,
201+
string valuePropertyName,
202+
string valueStringPropertyName,
203+
string valueTypePropertyName)
160204
{
161-
var token = new JObject();
205+
var token = new JObject
206+
{
207+
{unitPropertyName, "PowerUnit.Watt"},
208+
{valuePropertyName, 10.2365m},
209+
{valueStringPropertyName, 10.2365m.ToString(CultureInfo.InvariantCulture)},
210+
{valueTypePropertyName, "decimal"}
211+
};
162212

163-
token.Add(unitPropertyName, "PowerUnit.Watt");
164-
token.Add(valuePropertyName, 10.2365D);
165213

166-
var result = _sut.Test_ReadValueUnit(token);
214+
var result = _sut.Test_ReadDecimalValueUnit(token);
167215

168216
Assert.NotNull(result);
169217
Assert.Equal("PowerUnit.Watt", result?.Unit);
170-
Assert.Equal(10.2365D, result?.Value);
218+
Assert.Equal(10.2365m, result?.Value);
171219
}
172220

173221
/// <summary>
@@ -180,30 +228,58 @@ private class TestConverter : UnitsNetBaseJsonConverter<string>
180228
public override void WriteJson(JsonWriter writer, string value, JsonSerializer serializer) => throw new NotImplementedException();
181229
public override string ReadJson(JsonReader reader, Type objectType, string existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
182230

183-
public (string Unit, double Value) Test_ConvertIQuantity(IQuantity value)
231+
public (string Unit, double Value) Test_ConvertDoubleIQuantity(IQuantity value)
184232
{
185233
var result = ConvertIQuantity(value);
186-
187234
return (result.Unit, result.Value);
188235
}
189236

190-
public IQuantity Test_ConvertValueUnit(string unit, double value) => Test_ConvertValueUnit(new ValueUnit() {Unit = unit, Value = value});
237+
public (string Unit, decimal Value) Test_ConvertDecimalIQuantity(IQuantity value)
238+
{
239+
var result = ConvertIQuantity(value);
240+
if (result is ExtendedValueUnit {ValueType: "decimal"} decimalResult)
241+
{
242+
return (result.Unit, decimal.Parse(decimalResult.ValueString));
243+
}
244+
245+
throw new ArgumentException("The quantity does not have a decimal value", nameof(value));
246+
}
247+
248+
public IQuantity Test_ConvertDoubleValueUnit(string unit, double value) => Test_ConvertValueUnit(new ValueUnit {Unit = unit, Value = value});
249+
250+
public IQuantity Test_ConvertDecimalValueUnit(string unit, decimal value) => Test_ConvertValueUnit(new ExtendedValueUnit
251+
{
252+
Unit = unit, Value = (double) value, ValueString = value.ToString(CultureInfo.InvariantCulture), ValueType = "decimal"
253+
});
254+
191255
public IQuantity Test_ConvertValueUnit() => Test_ConvertValueUnit(null);
192256
private IQuantity Test_ConvertValueUnit(ValueUnit valueUnit) => ConvertValueUnit(valueUnit);
193257

194258
public JsonSerializer Test_CreateLocalSerializer(JsonSerializer serializer) => CreateLocalSerializer(serializer, this);
195259

196-
public (string Unit, double Value)? Test_ReadValueUnit(JToken jsonToken)
260+
public (string Unit, double Value)? Test_ReadDoubleValueUnit(JToken jsonToken)
197261
{
198262
var result = ReadValueUnit(jsonToken);
199-
200263
if (result == null)
201264
{
202265
return null;
203266
}
204267

205268
return (result.Unit, result.Value);
206269
}
270+
271+
public (string Unit, decimal Value)? Test_ReadDecimalValueUnit(JToken jsonToken)
272+
{
273+
var result = ReadValueUnit(jsonToken);
274+
275+
if (result is ExtendedValueUnit {ValueType: "decimal"} decimalResult)
276+
{
277+
return (result.Unit, decimal.Parse(decimalResult.ValueString));
278+
}
279+
280+
return null;
281+
}
282+
207283
}
208284
}
209285
}

UnitsNet.Serialization.JsonNet.Tests/UnitsNetIQuantityJsonConverterTest.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,34 @@ public void UnitsNetIQuantityJsonConverter_WriteJson_works_with_NULL_value()
5656
}
5757

5858
[Fact]
59-
public void UnitsNetIQuantityJsonConverter_WriteJson_works_as_expected()
59+
public void UnitsNetIQuantityJsonConverter_WriteJson_works_with_double_quantity()
60+
{
61+
var result = new StringBuilder();
62+
63+
using (var stringWriter = new StringWriter(result))
64+
using(var writer = new JsonTextWriter(stringWriter))
65+
{
66+
_sut.WriteJson(writer, Length.FromMeters(10.2365D), JsonSerializer.CreateDefault());
67+
}
68+
69+
Assert.Equal("{\"Unit\":\"LengthUnit.Meter\",\"Value\":10.2365}", result.ToString());
70+
}
71+
72+
[Theory]
73+
[InlineData(10.2365, "10.2365", "10.2365")]
74+
[InlineData(10, "10.0", "10")] // Json.NET adds .0
75+
public void UnitsNetIQuantityJsonConverter_WriteJson_works_with_decimal_quantity(decimal value, string expectedValue, string expectedValueString)
6076
{
6177
var result = new StringBuilder();
6278

6379
using (var stringWriter = new StringWriter(result))
6480
using(var writer = new JsonTextWriter(stringWriter))
6581
{
66-
_sut.WriteJson(writer, Power.FromWatts(10.2365D), JsonSerializer.CreateDefault());
82+
_sut.WriteJson(writer, Power.FromWatts(value), JsonSerializer.CreateDefault());
6783
}
6884

69-
Assert.Equal("{\"Unit\":\"PowerUnit.Watt\",\"Value\":10.2365}", result.ToString());
85+
Assert.Equal($"{{\"Unit\":\"PowerUnit.Watt\",\"Value\":{expectedValue},\"ValueString\":\"{expectedValueString}\",\"ValueType\":\"decimal\"}}",
86+
result.ToString());
7087
}
7188

7289
[Fact]

UnitsNet.Serialization.JsonNet.Tests/UnitsNetJsonDeserializationTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,46 @@ public void Information_CanDeserializeVeryLargeValues()
1919
Assert.Equal(original, deserialized);
2020
}
2121

22+
[Fact]
23+
public void Information_CanDeserializeMaxValue()
24+
{
25+
var original = Information.MaxValue;
26+
var json = SerializeObject(original);
27+
var deserialized = DeserializeObject<Information>(json);
28+
29+
Assert.Equal(original, deserialized);
30+
}
31+
32+
[Fact]
33+
public void Information_CanDeserializeMinValue()
34+
{
35+
var original = Information.MinValue;
36+
var json = SerializeObject(original);
37+
var deserialized = DeserializeObject<Information>(json);
38+
39+
Assert.Equal(original, deserialized);
40+
}
41+
42+
[Fact]
43+
public void Length_CanDeserializeMaxValue()
44+
{
45+
var original = Length.MaxValue;
46+
var json = SerializeObject(original);
47+
var deserialized = DeserializeObject<Length>(json);
48+
49+
Assert.Equal(original, deserialized);
50+
}
51+
52+
[Fact]
53+
public void Length_CanDeserializeMinValue()
54+
{
55+
var original = Length.MinValue;
56+
var json = SerializeObject(original);
57+
var deserialized = DeserializeObject<Length>(json);
58+
59+
Assert.Equal(original, deserialized);
60+
}
61+
2262
[Fact]
2363
public void Mass_ExpectJsonCorrectlyDeserialized()
2464
{

0 commit comments

Comments
 (0)