Skip to content

Commit bc3ee4d

Browse files
committed
serialize: Serialize constructed Value and Unit
Serialize with constructed Unit and _value instead of base unit and base unit value. Cast to QuantityValueDecimal for types that use decimal internally. NOTE: This mix of double and decimal representations is a bit messy right now and needs a better fix. We already discuss this topic about either moving everything to decimal or to better support multiple numeric types (float, double, long, decimal etc).
1 parent 50aeceb commit bc3ee4d

File tree

2 files changed

+121
-58
lines changed

2 files changed

+121
-58
lines changed

UnitsNet.Serialization.JsonNet.Tests/UnitsNetJsonConverterTests.cs

+16-5
Original file line numberDiff line numberDiff line change
@@ -51,24 +51,35 @@ public class Serialize : UnitsNetJsonConverterTests
5151
public void Information_CanSerializeVeryLargeValues()
5252
{
5353
Information i = Information.FromExabytes(1E+9);
54-
var expectedJson = "{\n \"Unit\": \"InformationUnit.Bit\",\n \"Value\": 8E+27\n}";
54+
var expectedJson = "{\n \"Unit\": \"InformationUnit.Exabyte\",\n \"Value\": 1000000000.0\n}";
5555

5656
string json = SerializeObject(i);
5757

5858
Assert.Equal(expectedJson, json);
5959
}
6060

6161
[Fact]
62-
public void Mass_ExpectKilogramsUsedAsBaseValueAndUnit()
62+
public void Mass_ExpectConstructedValueAndUnit()
6363
{
6464
Mass mass = Mass.FromPounds(200);
65-
var expectedJson = "{\n \"Unit\": \"MassUnit.Kilogram\",\n \"Value\": 90.718474\n}";
65+
var expectedJson = "{\n \"Unit\": \"MassUnit.Pound\",\n \"Value\": 200.0\n}";
6666

6767
string json = SerializeObject(mass);
6868

6969
Assert.Equal(expectedJson, json);
7070
}
7171

72+
[Fact]
73+
public void Information_ExpectConstructedValueAndUnit()
74+
{
75+
Information quantity = Information.FromKilobytes(54);
76+
var expectedJson = "{\n \"Unit\": \"InformationUnit.Kilobyte\",\n \"Value\": 54.0\n}";
77+
78+
string json = SerializeObject(quantity);
79+
80+
Assert.Equal(expectedJson, json);
81+
}
82+
7283
[Fact]
7384
public void NonNullNullableValue_ExpectJsonUnaffected()
7485
{
@@ -122,7 +133,7 @@ public void NullValue_ExpectJsonContainsNullString()
122133
public void Ratio_ExpectDecimalFractionsUsedAsBaseValueAndUnit()
123134
{
124135
Ratio ratio = Ratio.FromPartsPerThousand(250);
125-
var expectedJson = "{\n \"Unit\": \"RatioUnit.DecimalFraction\",\n \"Value\": 0.25\n}";
136+
var expectedJson = "{\n \"Unit\": \"RatioUnit.PartPerThousand\",\n \"Value\": 250.0\n}";
126137

127138
string json = SerializeObject(ratio);
128139

@@ -376,4 +387,4 @@ private class TestObjWithThreeIComparable
376387
public IComparable Value3 { get; set; }
377388
}
378389
}
379-
}
390+
}

UnitsNet.Serialization.JsonNet/UnitsNetJsonConverter.cs

+105-53
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
// THE SOFTWARE.
2121

2222
using System;
23-
using System.Globalization;
2423
using System.Linq;
2524
using System.Reflection;
2625
using JetBrains.Annotations;
@@ -29,9 +28,10 @@
2928

3029
namespace UnitsNet.Serialization.JsonNet
3130
{
31+
/// <inheritdoc />
3232
/// <summary>
33-
/// A JSON.net <see cref="JsonConverter" /> for converting to/from JSON and Units.NET
34-
/// units like <see cref="Length" /> and <see cref="Mass" />.
33+
/// A JSON.net <see cref="T:Newtonsoft.Json.JsonConverter" /> for converting to/from JSON and Units.NET
34+
/// units like <see cref="T:UnitsNet.Length" /> and <see cref="T:UnitsNet.Mass" />.
3535
/// </summary>
3636
/// <remarks>
3737
/// Relies on reflection and the type names and namespaces as of 3.x.x of Units.NET.
@@ -42,6 +42,11 @@ namespace UnitsNet.Serialization.JsonNet
4242
/// </remarks>
4343
public class UnitsNetJsonConverter : JsonConverter
4444
{
45+
/// <summary>
46+
/// Numeric value field of a quantity, typically of type double or decimal.
47+
/// </summary>
48+
private const string ValueFieldName = "_value";
49+
4550
/// <summary>
4651
/// Reads the JSON representation of the object.
4752
/// </summary>
@@ -61,9 +66,8 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
6166
return reader.Value;
6267
}
6368
object obj = TryDeserializeIComparable(reader, serializer);
64-
var vu = obj as ValueUnit;
6569
// A null System.Nullable value or a comparable type was deserialized so return this
66-
if (vu == null)
70+
if (!(obj is ValueUnit vu))
6771
{
6872
return obj;
6973
}
@@ -73,13 +77,13 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
7377
string unitEnumValue = vu.Unit.Split('.')[1];
7478

7579
// "MassUnit" => "Mass"
76-
string unitTypeName = unitEnumTypeName.Substring(0, unitEnumTypeName.Length - "Unit".Length);
80+
string quantityTypeName = unitEnumTypeName.Substring(0, unitEnumTypeName.Length - "Unit".Length);
7781

7882
// "UnitsNet.Units.MassUnit,UnitsNet"
7983
string unitEnumTypeAssemblyQualifiedName = "UnitsNet.Units." + unitEnumTypeName + ",UnitsNet";
8084

8185
// "UnitsNet.Mass,UnitsNet"
82-
string unitTypeAssemblyQualifiedName = "UnitsNet." + unitTypeName + ",UnitsNet";
86+
string quantityTypeAssemblyQualifiedName = "UnitsNet." + quantityTypeName + ",UnitsNet";
8387

8488
// -- see http://stackoverflow.com/a/6465096/1256096 for details
8589
Type reflectedUnitEnumType = Type.GetType(unitEnumTypeAssemblyQualifiedName);
@@ -90,18 +94,18 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
9094
throw ex;
9195
}
9296

93-
Type reflectedUnitType = Type.GetType(unitTypeAssemblyQualifiedName);
97+
Type reflectedUnitType = Type.GetType(quantityTypeAssemblyQualifiedName);
9498
if (reflectedUnitType == null)
9599
{
96100
var ex = new UnitsNetException("Unable to find unit type.");
97-
ex.Data["type"] = unitTypeAssemblyQualifiedName;
101+
ex.Data["type"] = quantityTypeAssemblyQualifiedName;
98102
throw ex;
99103
}
100104

101-
object unit = Enum.Parse(reflectedUnitEnumType, unitEnumValue);
105+
object unitValue = Enum.Parse(reflectedUnitEnumType, unitEnumValue);
102106

103107
// Mass.From() method, assume no overloads exist
104-
var fromMethod = reflectedUnitType
108+
MethodInfo fromMethod = reflectedUnitType
105109
#if (NETSTANDARD1_0)
106110
.GetTypeInfo()
107111
.GetDeclaredMethods("From")
@@ -112,14 +116,40 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
112116
!m.ReturnType.IsGenericType);
113117
#endif
114118

115-
// Implicit cast: we use this type to avoid explosion of method overloads to handle multiple number types
116-
QuantityValue quantityValue = vu.Value;
119+
// Either of type QuantityValue or QuantityValueDecimal
120+
object quantityValue = GetFromMethodValueArgument(fromMethod, vu.Value);
117121

118122
// Ex: Mass.From(55, MassUnit.Gram)
119123
// TODO: there is a possible loss of precision if base value requires higher precision than double can represent.
120124
// Example: Serializing Information.FromExabytes(100) then deserializing to Information
121125
// will likely return a very different result. Not sure how we can handle this?
122-
return fromMethod.Invoke(null, new[] {quantityValue, unit});
126+
return fromMethod.Invoke(null, new[] {quantityValue, unitValue});
127+
}
128+
129+
/// <summary>
130+
/// Returns numeric value wrapped as <see cref="QuantityValue"/> or <see cref="QuantityValueDecimal"/>, depending
131+
/// on what type the first parameter. Two examples are <see cref="Mass.From(UnitsNet.QuantityValue,UnitsNet.Units.MassUnit)"/> and
132+
/// <see cref="Information.From(UnitsNet.QuantityValueDecimal,UnitsNet.Units.InformationUnit)"/>.
133+
/// </summary>
134+
/// <param name="fromMethod">The reflected From(value, unit) method.</param>
135+
/// <param name="value">The value to convert to the correct wrapper type.</param>
136+
/// <returns></returns>
137+
private static object GetFromMethodValueArgument(MethodInfo fromMethod, double value)
138+
{
139+
Type valueParameterType = fromMethod.GetParameters()[0].ParameterType;
140+
if (valueParameterType == typeof(QuantityValue))
141+
{
142+
// Implicit cast: we use this type to avoid explosion of method overloads to handle multiple number types
143+
return (QuantityValue) value;
144+
}
145+
146+
if (valueParameterType == typeof(QuantityValueDecimal))
147+
{
148+
return (QuantityValueDecimal) value;
149+
}
150+
151+
throw new Exception(
152+
$"The first parameter of the reflected quantity From() method was expected to be either UnitsNet.QuantityValue or UnitsNet.QuantityValueDecimal, but was instead {valueParameterType}.");
123153
}
124154

125155
private static object TryDeserializeIComparable(JsonReader reader, JsonSerializer serializer)
@@ -147,77 +177,99 @@ private static object TryDeserializeIComparable(JsonReader reader, JsonSerialize
147177
/// Writes the JSON representation of the object.
148178
/// </summary>
149179
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter" /> to write to.</param>
150-
/// <param name="value">The value to write.</param>
180+
/// <param name="obj">The value to write.</param>
151181
/// <param name="serializer">The calling serializer.</param>
152182
/// <exception cref="UnitsNetException">Can't serialize 'null' value.</exception>
153-
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
183+
public override void WriteJson(JsonWriter writer, object obj, JsonSerializer serializer)
154184
{
155-
Type unitType = value.GetType();
185+
Type quantityType = obj.GetType();
156186

157187
// ValueUnit should be written as usual (but read in a custom way)
158-
if(unitType == typeof(ValueUnit))
188+
if(quantityType == typeof(ValueUnit))
159189
{
160190
JsonSerializer localSerializer = new JsonSerializer()
161191
{
162192
TypeNameHandling = serializer.TypeNameHandling,
163193
};
164-
JToken t = JToken.FromObject(value, localSerializer);
194+
JToken t = JToken.FromObject(obj, localSerializer);
165195

166196
t.WriteTo(writer);
167197
return;
168198
}
199+
200+
object quantityValue = GetValueOfQuantity(obj, quantityType); // double or decimal value
201+
string quantityUnitName = GetQuantityUnitName(obj, quantityType); // Example: "MassUnit.Kilogram"
202+
203+
serializer.Serialize(writer, new ValueUnit
204+
{
205+
// This might throw OverflowException for very large values?
206+
// TODO Should we serialize long, decimal and long differently?
207+
Value = Convert.ToDouble(quantityValue),
208+
Unit = quantityUnitName
209+
});
210+
}
211+
212+
private static string GetQuantityUnitName(object obj, Type quantityType)
213+
{
214+
// Mass => "MassUnit.Kilogram"
215+
PropertyInfo unitProperty = quantityType
216+
#if (NETSTANDARD1_0)
217+
.GetTypeInfo()
218+
.GetDeclaredProperty("Unit");
219+
#else
220+
.GetProperty("BaseUnit");
221+
#endif
222+
223+
// Unit property value
224+
var quantityUnit = (Enum) unitProperty.GetValue(obj, null); // MassUnit.Kilogram
225+
Type unitType = quantityUnit.GetType(); // MassUnit
226+
string quantityUnitName = $"{unitType.Name}.{quantityUnit}"; // "MassUnit.Kilogram"
227+
return quantityUnitName;
228+
}
229+
230+
private static object GetValueOfQuantity(object value, Type quantityType)
231+
{
232+
FieldInfo valueField = GetPrivateInstanceField(quantityType, ValueFieldName);
233+
234+
// Unit base type can be double, long or decimal,
235+
// so make sure we serialize the real type to avoid
236+
// loss of precision
237+
object quantityValue = valueField.GetValue(value);
238+
return quantityValue;
239+
}
240+
241+
private static FieldInfo GetPrivateInstanceField(Type quantityType, string fieldName)
242+
{
169243
FieldInfo baseValueField;
170244
try
171245
{
172-
baseValueField = unitType
246+
baseValueField = quantityType
173247
#if (NETSTANDARD1_0)
174248
.GetTypeInfo()
175-
176249
.DeclaredFields
177-
.SingleOrDefault(f => !f.IsPublic && !f.IsStatic);
250+
.Where(f => !f.IsPublic && !f.IsStatic)
178251
#else
179252
.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)
180-
.SingleOrDefault();
181253
#endif
254+
.SingleOrDefault(f => f.Name == fieldName);
182255
}
183256
catch (InvalidOperationException)
184257
{
185-
var ex = new UnitsNetException("Expected exactly 1 private field, but found multiple.");
186-
ex.Data["type"] = unitType;
258+
var ex = new UnitsNetException($"Expected exactly one private field named [{fieldName}], but found multiple.");
259+
ex.Data["type"] = quantityType;
260+
ex.Data["fieldName"] = fieldName;
187261
throw ex;
188262
}
263+
189264
if (baseValueField == null)
190265
{
191266
var ex = new UnitsNetException("No private fields found in type.");
192-
ex.Data["type"] = unitType;
267+
ex.Data["type"] = quantityType;
268+
ex.Data["fieldName"] = fieldName;
193269
throw ex;
194270
}
195-
// Unit base type can be double, long or decimal,
196-
// so make sure we serialize the real type to avoid
197-
// loss of precision
198-
object baseValue = baseValueField.GetValue(value);
199-
200-
// Mass => "MassUnit.Kilogram"
201-
PropertyInfo baseUnitPropInfo = unitType
202-
#if (NETSTANDARD1_0)
203-
.GetTypeInfo()
204-
.GetDeclaredProperty("BaseUnit");
205-
#else
206-
.GetProperty("BaseUnit");
207-
#endif
208-
209-
// Read static BaseUnit property value
210-
var baseUnitEnumValue = (Enum) baseUnitPropInfo.GetValue(null, null);
211-
Type baseUnitType = baseUnitEnumValue.GetType();
212-
string baseUnit = $"{baseUnitType.Name}.{baseUnitEnumValue}";
213271

214-
serializer.Serialize(writer, new ValueUnit
215-
{
216-
// This might throw OverflowException for very large values?
217-
// TODO Should we serialize long, decimal and long differently?
218-
Value = Convert.ToDouble(baseValue),
219-
Unit = baseUnit
220-
});
272+
return baseValueField;
221273
}
222274

223275
/// <summary>
@@ -261,7 +313,7 @@ public override bool CanConvert(Type objectType)
261313
/// </summary>
262314
/// <param name="objectType">Type of the object.</param>
263315
/// <returns><c>true</c> if the object type is nullable; otherwise <c>false</c>.</returns>
264-
protected bool IsNullable(Type objectType)
316+
private static bool IsNullable(Type objectType)
265317
{
266318
return Nullable.GetUnderlyingType(objectType) != null;
267319
}
@@ -280,4 +332,4 @@ protected virtual bool CanConvertNullable(Type objectType)
280332

281333
#endregion
282334
}
283-
}
335+
}

0 commit comments

Comments
 (0)