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 ;
29
28
30
29
namespace UnitsNet . Serialization . JsonNet
31
30
{
31
+ /// <inheritdoc />
32
32
/// <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" />.
35
35
/// </summary>
36
36
/// <remarks>
37
37
/// 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
42
42
/// </remarks>
43
43
public class UnitsNetJsonConverter : JsonConverter
44
44
{
45
+ /// <summary>
46
+ /// Numeric value field of a quantity, typically of type double or decimal.
47
+ /// </summary>
48
+ private const string ValueFieldName = "_value" ;
49
+
45
50
/// <summary>
46
51
/// Reads the JSON representation of the object.
47
52
/// </summary>
@@ -61,9 +66,8 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
61
66
return reader . Value ;
62
67
}
63
68
object obj = TryDeserializeIComparable ( reader , serializer ) ;
64
- var vu = obj as ValueUnit ;
65
69
// A null System.Nullable value or a comparable type was deserialized so return this
66
- if ( vu == null )
70
+ if ( ! ( obj is ValueUnit vu ) )
67
71
{
68
72
return obj ;
69
73
}
@@ -73,13 +77,13 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
73
77
string unitEnumValue = vu . Unit . Split ( '.' ) [ 1 ] ;
74
78
75
79
// "MassUnit" => "Mass"
76
- string unitTypeName = unitEnumTypeName . Substring ( 0 , unitEnumTypeName . Length - "Unit" . Length ) ;
80
+ string quantityTypeName = unitEnumTypeName . Substring ( 0 , unitEnumTypeName . Length - "Unit" . Length ) ;
77
81
78
82
// "UnitsNet.Units.MassUnit,UnitsNet"
79
83
string unitEnumTypeAssemblyQualifiedName = "UnitsNet.Units." + unitEnumTypeName + ",UnitsNet" ;
80
84
81
85
// "UnitsNet.Mass,UnitsNet"
82
- string unitTypeAssemblyQualifiedName = "UnitsNet." + unitTypeName + ",UnitsNet" ;
86
+ string quantityTypeAssemblyQualifiedName = "UnitsNet." + quantityTypeName + ",UnitsNet" ;
83
87
84
88
// -- see http://stackoverflow.com/a/6465096/1256096 for details
85
89
Type reflectedUnitEnumType = Type . GetType ( unitEnumTypeAssemblyQualifiedName ) ;
@@ -90,18 +94,18 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
90
94
throw ex ;
91
95
}
92
96
93
- Type reflectedUnitType = Type . GetType ( unitTypeAssemblyQualifiedName ) ;
97
+ Type reflectedUnitType = Type . GetType ( quantityTypeAssemblyQualifiedName ) ;
94
98
if ( reflectedUnitType == null )
95
99
{
96
100
var ex = new UnitsNetException ( "Unable to find unit type." ) ;
97
- ex . Data [ "type" ] = unitTypeAssemblyQualifiedName ;
101
+ ex . Data [ "type" ] = quantityTypeAssemblyQualifiedName ;
98
102
throw ex ;
99
103
}
100
104
101
- object unit = Enum . Parse ( reflectedUnitEnumType , unitEnumValue ) ;
105
+ object unitValue = Enum . Parse ( reflectedUnitEnumType , unitEnumValue ) ;
102
106
103
107
// Mass.From() method, assume no overloads exist
104
- var fromMethod = reflectedUnitType
108
+ MethodInfo fromMethod = reflectedUnitType
105
109
#if ( NETSTANDARD1_0 )
106
110
. GetTypeInfo ( )
107
111
. GetDeclaredMethods ( "From" )
@@ -112,14 +116,40 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
112
116
! m . ReturnType . IsGenericType ) ;
113
117
#endif
114
118
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 ) ;
117
121
118
122
// Ex: Mass.From(55, MassUnit.Gram)
119
123
// TODO: there is a possible loss of precision if base value requires higher precision than double can represent.
120
124
// Example: Serializing Information.FromExabytes(100) then deserializing to Information
121
125
// 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 } .") ;
123
153
}
124
154
125
155
private static object TryDeserializeIComparable ( JsonReader reader , JsonSerializer serializer )
@@ -147,77 +177,99 @@ private static object TryDeserializeIComparable(JsonReader reader, JsonSerialize
147
177
/// Writes the JSON representation of the object.
148
178
/// </summary>
149
179
/// <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>
151
181
/// <param name="serializer">The calling serializer.</param>
152
182
/// <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 )
154
184
{
155
- Type unitType = value . GetType ( ) ;
185
+ Type quantityType = obj . GetType ( ) ;
156
186
157
187
// ValueUnit should be written as usual (but read in a custom way)
158
- if ( unitType == typeof ( ValueUnit ) )
188
+ if ( quantityType == typeof ( ValueUnit ) )
159
189
{
160
190
JsonSerializer localSerializer = new JsonSerializer ( )
161
191
{
162
192
TypeNameHandling = serializer . TypeNameHandling ,
163
193
} ;
164
- JToken t = JToken . FromObject ( value , localSerializer ) ;
194
+ JToken t = JToken . FromObject ( obj , localSerializer ) ;
165
195
166
196
t . WriteTo ( writer ) ;
167
197
return ;
168
198
}
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
+ {
169
243
FieldInfo baseValueField ;
170
244
try
171
245
{
172
- baseValueField = unitType
246
+ baseValueField = quantityType
173
247
#if ( NETSTANDARD1_0 )
174
248
. GetTypeInfo ( )
175
-
176
249
. DeclaredFields
177
- . SingleOrDefault ( f => ! f . IsPublic && ! f . IsStatic ) ;
250
+ . Where ( f => ! f . IsPublic && ! f . IsStatic )
178
251
#else
179
252
. GetFields ( BindingFlags . NonPublic | BindingFlags . Instance | BindingFlags . DeclaredOnly )
180
- . SingleOrDefault ( ) ;
181
253
#endif
254
+ . SingleOrDefault ( f => f . Name == fieldName ) ;
182
255
}
183
256
catch ( InvalidOperationException )
184
257
{
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 ;
187
261
throw ex ;
188
262
}
263
+
189
264
if ( baseValueField == null )
190
265
{
191
266
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 ;
193
269
throw ex ;
194
270
}
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 } ";
213
271
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 ;
221
273
}
222
274
223
275
/// <summary>
@@ -261,7 +313,7 @@ public override bool CanConvert(Type objectType)
261
313
/// </summary>
262
314
/// <param name="objectType">Type of the object.</param>
263
315
/// <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 )
265
317
{
266
318
return Nullable . GetUnderlyingType ( objectType ) != null ;
267
319
}
@@ -280,4 +332,4 @@ protected virtual bool CanConvertNullable(Type objectType)
280
332
281
333
#endregion
282
334
}
283
- }
335
+ }
0 commit comments