Skip to content

Commit c5af7a1

Browse files
committed
Fix JSON serialization of quantities with decimal values
Add IDecimalQuantity interface to expose the decimal value Serialize decimal values as string to keep number of decimal places
1 parent 4993a2a commit c5af7a1

File tree

10 files changed

+275
-74
lines changed

10 files changed

+275
-74
lines changed

CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs

+13-1
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.
@@ -268,6 +275,11 @@ private void GenerateProperties()
268275
Writer.WL(@"
269276
double IQuantity.Value => (double) _value;
270277
");
278+
if (_quantity.BaseType == "decimal")
279+
Writer.WL(@"
280+
/// <inheritdoc cref=""IDecimalQuantity.Value""/>
281+
decimal IDecimalQuantity.Value => _value;
282+
");
271283

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

UnitsNet.Serialization.JsonNet.Tests/UnitsNetBaseJsonConverterTest.cs

+92-35
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,35 @@ 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);
113122

114-
var result = _sut.Test_ReadValueUnit(token);
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"}};
132+
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);
119138
}
120139

121140
[Fact]
122141
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_with_empty_token()
123142
{
124143
var token = new JObject();
125144

126-
var result = _sut.Test_ReadValueUnit(token);
145+
var result = _sut.Test_ReadDoubleValueUnit(token);
127146

128147
Assert.Null(result);
129148
}
@@ -142,32 +161,40 @@ public void UnitsNetBaseJsonConverter_ReadValueUnit_returns_null_when_unit_or_va
142161

143162
if (withValue)
144163
{
145-
token.Add("Value", 10.2365D);
164+
token.Add("Value", 10.2365m);
146165
}
147166

148-
var result = _sut.Test_ReadValueUnit(token);
167+
var result = _sut.Test_ReadDecimalValueUnit(token);
149168

150169
Assert.Null(result);
151170
}
152171

153172
[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)
173+
[InlineData("Unit", "Value", "ValueString", "ValueType")]
174+
[InlineData("unit", "Value", "ValueString", "ValueType")]
175+
[InlineData("Unit", "value", "valueString", "valueType")]
176+
[InlineData("unit", "value", "valueString", "valueType")]
177+
[InlineData("unIT", "vAlUe", "vAlUeString", "vAlUeType")]
178+
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_case_insensitive(
179+
string unitPropertyName,
180+
string valuePropertyName,
181+
string valueStringPropertyName,
182+
string valueTypePropertyName)
160183
{
161-
var token = new JObject();
184+
var token = new JObject
185+
{
186+
{unitPropertyName, "PowerUnit.Watt"},
187+
{valuePropertyName, 10.2365m},
188+
{valueStringPropertyName, 10.2365m.ToString(CultureInfo.InvariantCulture)},
189+
{valueTypePropertyName, "decimal"}
190+
};
162191

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

166-
var result = _sut.Test_ReadValueUnit(token);
193+
var result = _sut.Test_ReadDecimalValueUnit(token);
167194

168195
Assert.NotNull(result);
169196
Assert.Equal("PowerUnit.Watt", result?.Unit);
170-
Assert.Equal(10.2365D, result?.Value);
197+
Assert.Equal(10.2365m, result?.Value);
171198
}
172199

173200
/// <summary>
@@ -180,30 +207,60 @@ private class TestConverter : UnitsNetBaseJsonConverter<string>
180207
public override void WriteJson(JsonWriter writer, string value, JsonSerializer serializer) => throw new NotImplementedException();
181208
public override string ReadJson(JsonReader reader, Type objectType, string existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
182209

183-
public (string Unit, double Value) Test_ConvertIQuantity(IQuantity value)
210+
public (string Unit, double Value) Test_ConvertDoubleIQuantity(IQuantity value)
184211
{
185212
var result = ConvertIQuantity(value);
186-
187213
return (result.Unit, result.Value);
214+
215+
throw new ArgumentException("The quantity does not have a double value", nameof(value));
216+
}
217+
218+
public (string Unit, decimal Value) Test_ConvertDecimalIQuantity(IQuantity value)
219+
{
220+
var result = ConvertIQuantity(value);
221+
if (result is ExtendedValueUnit {ValueType: "decimal"} decimalResult)
222+
{
223+
return (result.Unit, decimal.Parse(decimalResult.ValueString));
224+
}
225+
226+
throw new ArgumentException("The quantity does not have a decimal value", nameof(value));
188227
}
189228

190-
public IQuantity Test_ConvertValueUnit(string unit, double value) => Test_ConvertValueUnit(new ValueUnit() {Unit = unit, Value = value});
229+
public IQuantity Test_ConvertDoubleValueUnit(string unit, double value) => Test_ConvertValueUnit(new ValueUnit {Unit = unit, Value = value});
230+
231+
public IQuantity Test_ConvertDecimalValueUnit(string unit, decimal value) => Test_ConvertValueUnit(new ExtendedValueUnit
232+
{
233+
Unit = unit, Value = (double) value, ValueString = value.ToString(CultureInfo.InvariantCulture), ValueType = "decimal"
234+
});
235+
191236
public IQuantity Test_ConvertValueUnit() => Test_ConvertValueUnit(null);
192237
private IQuantity Test_ConvertValueUnit(ValueUnit valueUnit) => ConvertValueUnit(valueUnit);
193238

194239
public JsonSerializer Test_CreateLocalSerializer(JsonSerializer serializer) => CreateLocalSerializer(serializer, this);
195240

196-
public (string Unit, double Value)? Test_ReadValueUnit(JToken jsonToken)
241+
public (string Unit, double Value)? Test_ReadDoubleValueUnit(JToken jsonToken)
197242
{
198243
var result = ReadValueUnit(jsonToken);
199-
200244
if (result == null)
201245
{
202246
return null;
203247
}
204248

205249
return (result.Unit, result.Value);
206250
}
251+
252+
public (string Unit, decimal Value)? Test_ReadDecimalValueUnit(JToken jsonToken)
253+
{
254+
var result = ReadValueUnit(jsonToken);
255+
256+
if (result is ExtendedValueUnit {ValueType: "decimal"} decimalResult)
257+
{
258+
return (result.Unit, decimal.Parse(decimalResult.ValueString));
259+
}
260+
261+
return null;
262+
}
263+
207264
}
208265
}
209266
}

UnitsNet.Serialization.JsonNet.Tests/UnitsNetIQuantityJsonConverterTest.cs

+20-3
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

+40
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
{

UnitsNet.Serialization.JsonNet.Tests/UnitsNetJsonSerializationTests.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public sealed class UnitsNetJsonSerializationTests : UnitsNetJsonBaseTest
1212
public void Information_CanSerializeVeryLargeValues()
1313
{
1414
Information i = Information.FromExabytes(1E+9);
15-
var expectedJson = "{\n \"Unit\": \"InformationUnit.Exabyte\",\n \"Value\": 1000000000.0\n}";
15+
var expectedJson = "{\n \"Unit\": \"InformationUnit.Exabyte\",\n \"Value\": 1000000000.0,\n \"ValueString\": \"1000000000\",\n \"ValueType\": \"decimal\"\n}";
1616

1717
string json = SerializeObject(i);
1818

@@ -34,7 +34,7 @@ public void Mass_ExpectConstructedValueAndUnit()
3434
public void Information_ExpectConstructedValueAndUnit()
3535
{
3636
Information quantity = Information.FromKilobytes(54);
37-
var expectedJson = "{\n \"Unit\": \"InformationUnit.Kilobyte\",\n \"Value\": 54.0\n}";
37+
var expectedJson = "{\n \"Unit\": \"InformationUnit.Kilobyte\",\n \"Value\": 54.0,\n \"ValueString\": \"54\",\n \"ValueType\": \"decimal\"\n}";
3838

3939
string json = SerializeObject(quantity);
4040

@@ -148,7 +148,7 @@ public void MultiDimArrayValue_ExpectJsonArray()
148148
"]";
149149

150150
string json = SerializeObject(testObj);
151-
151+
152152
Assert.Equal(expectedJson, json);
153153
}
154154

0 commit comments

Comments
 (0)