20
20
// THE SOFTWARE.
21
21
22
22
using System ;
23
- using System . Globalization ;
24
23
using System . Linq ;
25
24
using System . Reflection ;
26
25
using JetBrains . Annotations ;
27
26
using Newtonsoft . Json ;
28
27
using Newtonsoft . Json . Linq ;
28
+ using UnitsNet . InternalHelpers ;
29
29
30
30
namespace UnitsNet . Serialization . JsonNet
31
31
{
32
+ /// <inheritdoc />
32
33
/// <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" />.
34
+ /// A JSON.net <see cref="T:Newtonsoft.Json. JsonConverter" /> for converting to/from JSON and Units.NET
35
+ /// units like <see cref="T:UnitsNet. Length" /> and <see cref="T:UnitsNet. Mass" />.
35
36
/// </summary>
36
37
/// <remarks>
37
38
/// Relies on reflection and the type names and namespaces as of 3.x.x of Units.NET.
@@ -42,6 +43,11 @@ namespace UnitsNet.Serialization.JsonNet
42
43
/// </remarks>
43
44
public class UnitsNetJsonConverter : JsonConverter
44
45
{
46
+ /// <summary>
47
+ /// Numeric value field of a quantity, typically of type double or decimal.
48
+ /// </summary>
49
+ private const string ValueFieldName = "_value" ;
50
+
45
51
/// <summary>
46
52
/// Reads the JSON representation of the object.
47
53
/// </summary>
@@ -61,9 +67,8 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
61
67
return reader . Value ;
62
68
}
63
69
object obj = TryDeserializeIComparable ( reader , serializer ) ;
64
- var vu = obj as ValueUnit ;
65
70
// A null System.Nullable value or a comparable type was deserialized so return this
66
- if ( vu == null )
71
+ if ( ! ( obj is ValueUnit vu ) )
67
72
{
68
73
return obj ;
69
74
}
@@ -73,53 +78,87 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
73
78
string unitEnumValue = vu . Unit . Split ( '.' ) [ 1 ] ;
74
79
75
80
// "MassUnit" => "Mass"
76
- string unitTypeName = unitEnumTypeName . Substring ( 0 , unitEnumTypeName . Length - "Unit" . Length ) ;
81
+ string quantityTypeName = unitEnumTypeName . Substring ( 0 , unitEnumTypeName . Length - "Unit" . Length ) ;
77
82
78
83
// "UnitsNet.Units.MassUnit,UnitsNet"
79
84
string unitEnumTypeAssemblyQualifiedName = "UnitsNet.Units." + unitEnumTypeName + ",UnitsNet" ;
80
85
81
86
// "UnitsNet.Mass,UnitsNet"
82
- string unitTypeAssemblyQualifiedName = "UnitsNet." + unitTypeName + ",UnitsNet" ;
87
+ string quantityTypeAssemblyQualifiedName = "UnitsNet." + quantityTypeName + ",UnitsNet" ;
83
88
84
89
// -- see http://stackoverflow.com/a/6465096/1256096 for details
85
- Type reflectedUnitEnumType = Type . GetType ( unitEnumTypeAssemblyQualifiedName ) ;
86
- if ( reflectedUnitEnumType == null )
90
+ Type unitEnumType = Type . GetType ( unitEnumTypeAssemblyQualifiedName ) ;
91
+ if ( unitEnumType == null )
87
92
{
88
93
var ex = new UnitsNetException ( "Unable to find enum type." ) ;
89
94
ex . Data [ "type" ] = unitEnumTypeAssemblyQualifiedName ;
90
95
throw ex ;
91
96
}
92
97
93
- Type reflectedUnitType = Type . GetType ( unitTypeAssemblyQualifiedName ) ;
94
- if ( reflectedUnitType == null )
98
+ Type quantityType = Type . GetType ( quantityTypeAssemblyQualifiedName ) ;
99
+ if ( quantityType == null )
95
100
{
96
101
var ex = new UnitsNetException ( "Unable to find unit type." ) ;
97
- ex . Data [ "type" ] = unitTypeAssemblyQualifiedName ;
102
+ ex . Data [ "type" ] = quantityTypeAssemblyQualifiedName ;
98
103
throw ex ;
99
104
}
100
105
101
- object unit = Enum . Parse ( reflectedUnitEnumType , unitEnumValue ) ;
106
+ double value = vu . Value ;
107
+ object unitValue = Enum . Parse ( unitEnumType , unitEnumValue ) ; // Ex: MassUnit.Kilogram
102
108
103
- // Mass.From() method, assume no overloads exist
104
- var fromMethod = reflectedUnitType
105
- #if ( NETSTANDARD1_0 )
106
- . GetTypeInfo ( )
107
- . GetDeclaredMethods ( "From" )
108
- . Single ( m => ! m . ReturnType . IsConstructedGenericType ) ;
109
- #else
110
- . GetMethods ( )
111
- . Single ( m => m . Name . Equals ( "From" , StringComparison . InvariantCulture ) &&
112
- ! m . ReturnType . IsGenericType ) ;
113
- #endif
109
+ return CreateQuantity ( quantityType , value , unitValue ) ;
110
+ }
114
111
115
- // Implicit cast: we use this type to avoid explosion of method overloads to handle multiple number types
116
- QuantityValue quantityValue = vu . Value ;
112
+ /// <summary>
113
+ /// Creates a quantity (ex: Mass) based on the reflected quantity type, a numeric value and a unit value (ex: MassUnit.Kilogram).
114
+ /// </summary>
115
+ /// <param name="quantityType">Type of quantity, such as <see cref="Mass"/>.</param>
116
+ /// <param name="value">Numeric value.</param>
117
+ /// <param name="unitValue">The unit, such as <see cref="MassUnit.Kilogram"/>.</param>
118
+ /// <returns>The constructed quantity, such as <see cref="Mass"/>.</returns>
119
+ private static object CreateQuantity ( Type quantityType , double value , object unitValue )
120
+ {
121
+ // We want the non-nullable return type, example candidates if quantity type is Mass:
122
+ // double Mass.From(double, MassUnit)
123
+ // double? Mass.From(double?, MassUnit)
124
+ MethodInfo notNullableFromMethod = quantityType
125
+ . GetDeclaredMethods ( )
126
+ . Single ( m => m . Name == "From" && Nullable . GetUnderlyingType ( m . ReturnType ) == null ) ;
127
+
128
+ // Either of type QuantityValue or QuantityValueDecimal
129
+ object quantityValue = GetFromMethodValueArgument ( notNullableFromMethod , value ) ;
117
130
118
131
// Ex: Mass.From(55, MassUnit.Gram)
119
132
// TODO: there is a possible loss of precision if base value requires higher precision than double can represent.
120
133
// Example: Serializing Information.FromExabytes(100) then deserializing to Information
121
134
// will likely return a very different result. Not sure how we can handle this?
122
- return fromMethod . Invoke ( null , new [ ] { quantityValue , unit } ) ;
135
+ return notNullableFromMethod . Invoke ( null , new [ ] { quantityValue , unitValue } ) ;
136
+ }
137
+
138
+ /// <summary>
139
+ /// Returns numeric value wrapped as <see cref="QuantityValue"/> or <see cref="QuantityValueDecimal"/>, depending
140
+ /// on what type the first parameter. Two examples are <see cref="Mass.From(UnitsNet.QuantityValue,UnitsNet.Units.MassUnit)"/> and
141
+ /// <see cref="Information.From(UnitsNet.QuantityValueDecimal,UnitsNet.Units.InformationUnit)"/>.
142
+ /// </summary>
143
+ /// <param name="fromMethod">The reflected From(value, unit) method.</param>
144
+ /// <param name="value">The value to convert to the correct wrapper type.</param>
145
+ /// <returns></returns>
146
+ private static object GetFromMethodValueArgument ( MethodInfo fromMethod , double value )
147
+ {
148
+ Type valueParameterType = fromMethod . GetParameters ( ) [ 0 ] . ParameterType ;
149
+ if ( valueParameterType == typeof ( QuantityValue ) )
150
+ {
151
+ // Implicit cast: we use this type to avoid explosion of method overloads to handle multiple number types
152
+ return ( QuantityValue ) value ;
153
+ }
154
+
155
+ if ( valueParameterType == typeof ( QuantityValueDecimal ) )
156
+ {
157
+ return ( QuantityValueDecimal ) value ;
158
+ }
159
+
160
+ throw new Exception (
161
+ $ "The first parameter of the reflected quantity From() method was expected to be either UnitsNet.QuantityValue or UnitsNet.QuantityValueDecimal, but was instead { valueParameterType } .") ;
123
162
}
124
163
125
164
private static object TryDeserializeIComparable ( JsonReader reader , JsonSerializer serializer )
@@ -147,77 +186,97 @@ private static object TryDeserializeIComparable(JsonReader reader, JsonSerialize
147
186
/// Writes the JSON representation of the object.
148
187
/// </summary>
149
188
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter" /> to write to.</param>
150
- /// <param name="value ">The value to write.</param>
189
+ /// <param name="obj ">The value to write.</param>
151
190
/// <param name="serializer">The calling serializer.</param>
152
191
/// <exception cref="UnitsNetException">Can't serialize 'null' value.</exception>
153
- public override void WriteJson ( JsonWriter writer , object value , JsonSerializer serializer )
192
+ public override void WriteJson ( JsonWriter writer , object obj , JsonSerializer serializer )
154
193
{
155
- Type unitType = value . GetType ( ) ;
194
+ Type quantityType = obj . GetType ( ) ;
156
195
157
196
// ValueUnit should be written as usual (but read in a custom way)
158
- if ( unitType == typeof ( ValueUnit ) )
197
+ if ( quantityType == typeof ( ValueUnit ) )
159
198
{
160
199
JsonSerializer localSerializer = new JsonSerializer ( )
161
200
{
162
201
TypeNameHandling = serializer . TypeNameHandling ,
163
202
} ;
164
- JToken t = JToken . FromObject ( value , localSerializer ) ;
203
+ JToken t = JToken . FromObject ( obj , localSerializer ) ;
165
204
166
205
t . WriteTo ( writer ) ;
167
206
return ;
168
207
}
208
+
209
+ object quantityValue = GetValueOfQuantity ( obj , quantityType ) ; // double or decimal value
210
+ string quantityUnitName = GetUnitFullNameOfQuantity ( obj , quantityType ) ; // Example: "MassUnit.Kilogram"
211
+
212
+ serializer . Serialize ( writer , new ValueUnit
213
+ {
214
+ // This might throw OverflowException for very large values?
215
+ // TODO Should we serialize long, decimal and long differently?
216
+ Value = Convert . ToDouble ( quantityValue ) ,
217
+ Unit = quantityUnitName
218
+ } ) ;
219
+ }
220
+
221
+ /// <summary>
222
+ /// Given quantity (ex: <see cref="Mass"/>), returns the full name (ex: "MassUnit.Kilogram") of the constructed unit given by the <see cref="Mass.Unit"/> property.
223
+ /// </summary>
224
+ /// <param name="obj">Quantity, such as <see cref="Mass"/>.</param>
225
+ /// <param name="quantityType">The type of <paramref name="obj"/>, passed in here to reuse a previous lookup.</param>
226
+ /// <returns>"MassUnit.Kilogram" for a mass quantity whose Unit property is MassUnit.Kilogram.</returns>
227
+ private static string GetUnitFullNameOfQuantity ( object obj , Type quantityType )
228
+ {
229
+ // Get value of Unit property
230
+ PropertyInfo unitProperty = quantityType . GetPropety ( "Unit" ) ;
231
+ Enum quantityUnit = ( Enum ) unitProperty . GetValue ( obj , null ) ; // MassUnit.Kilogram
232
+
233
+ Type unitType = quantityUnit . GetType ( ) ; // MassUnit
234
+ return $ "{ unitType . Name } .{ quantityUnit } "; // "MassUnit.Kilogram"
235
+ }
236
+
237
+ private static object GetValueOfQuantity ( object value , Type quantityType )
238
+ {
239
+ FieldInfo valueField = GetPrivateInstanceField ( quantityType , ValueFieldName ) ;
240
+
241
+ // Unit base type can be double, long or decimal,
242
+ // so make sure we serialize the real type to avoid
243
+ // loss of precision
244
+ object quantityValue = valueField . GetValue ( value ) ;
245
+ return quantityValue ;
246
+ }
247
+
248
+ private static FieldInfo GetPrivateInstanceField ( Type quantityType , string fieldName )
249
+ {
169
250
FieldInfo baseValueField ;
170
251
try
171
252
{
172
- baseValueField = unitType
253
+ baseValueField = quantityType
173
254
#if ( NETSTANDARD1_0 )
174
255
. GetTypeInfo ( )
175
-
176
256
. DeclaredFields
177
- . SingleOrDefault ( f => ! f . IsPublic && ! f . IsStatic ) ;
257
+ . Where ( f => ! f . IsPublic && ! f . IsStatic )
178
258
#else
179
259
. GetFields ( BindingFlags . NonPublic | BindingFlags . Instance | BindingFlags . DeclaredOnly )
180
- . SingleOrDefault ( ) ;
181
260
#endif
261
+ . SingleOrDefault ( f => f . Name == fieldName ) ;
182
262
}
183
263
catch ( InvalidOperationException )
184
264
{
185
- var ex = new UnitsNetException ( "Expected exactly 1 private field, but found multiple." ) ;
186
- ex . Data [ "type" ] = unitType ;
265
+ var ex = new UnitsNetException ( $ "Expected exactly one private field named [{ fieldName } ], but found multiple.") ;
266
+ ex . Data [ "type" ] = quantityType ;
267
+ ex . Data [ "fieldName" ] = fieldName ;
187
268
throw ex ;
188
269
}
270
+
189
271
if ( baseValueField == null )
190
272
{
191
273
var ex = new UnitsNetException ( "No private fields found in type." ) ;
192
- ex . Data [ "type" ] = unitType ;
274
+ ex . Data [ "type" ] = quantityType ;
275
+ ex . Data [ "fieldName" ] = fieldName ;
193
276
throw ex ;
194
277
}
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
278
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 } ";
213
-
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
- } ) ;
279
+ return baseValueField ;
221
280
}
222
281
223
282
/// <summary>
@@ -261,7 +320,7 @@ public override bool CanConvert(Type objectType)
261
320
/// </summary>
262
321
/// <param name="objectType">Type of the object.</param>
263
322
/// <returns><c>true</c> if the object type is nullable; otherwise <c>false</c>.</returns>
264
- protected bool IsNullable ( Type objectType )
323
+ private static bool IsNullable ( Type objectType )
265
324
{
266
325
return Nullable . GetUnderlyingType ( objectType ) != null ;
267
326
}
@@ -280,4 +339,4 @@ protected virtual bool CanConvertNullable(Type objectType)
280
339
281
340
#endregion
282
341
}
283
- }
342
+ }
0 commit comments