.NET 6, C# 10, System.Text.Json
Assuming a JsonElement that has a ValueKind of Number, the JsonElement's value could have an underlying type of int, decimal, double, etc. To find this underlying type, one could run the through a series of tests such as TryGetInt32(), TryGetInt64(), TryGetDouble(), etc.
Is this the best practice, or is there a more idiomatic way?
If you know the JSON number value will be an integer, then use GetInt64 /TryGetInt64.
GetInt32 or TryGetInt32 unless you know you can.Otherwise, just use Decimal, as in:JsonElement.TryGetDecimal and JsonElement.GetDecimal.
The .NET Decimal type can represent practically every JSON number value with full precision, regardless of if it's an integer number, e.g. { "value": 1234 } or a non-integer number, e.g. { "value": 12.34 } in the source JSON.
Number.MAX_SAFE_INTEGER (that's 253, or 9,007,199,254,740,991).
double values in it - in which case using TryGetDouble or GetDouble would be appropriate, so always check with your project requirements first.In general, avoid using IEEE-754 floating-point types (Single and Double) as they cannot accurately and precisely represent certain types of numbers which would prevent round-tripping and cause data loss or corruption (e.g. try doing ( 0.001f + 0.004f ) - 0.001f).
Assuming a
JsonElementthat has aValueKindofNumber, theJsonElement's value could have an underlying type ofint,decimal,double, etc.
This assumption is incorrect: the JsonElement type does not store or represent JSON number values using any specific .NET type: instead the JsonElement struct is simply a view over the source JsonDocument's serialized JSON data: so essentially a JsonElement is really just a (validated) run-of-text over a giant JSON string.
Therefore, when a JsonElement's ValueKind == JsonValueKind.Number it just means it wraps a sequence of characters directly representing the serialized JSON number value, so there is no "underlying" value representation.
...this means that you can use any of the TryGet... methods to get a .NET numeric value from a JSON number value: the only thing you need to consider is if the .NET type can validly represent the JSON number value or not: so...
null integer values under MAX_SAFE_INTEGER then use GetInt64 (no need for TryGetInt64).null integer values under 232 then use GetInt32 (no need for TryGetInt32).null or not, but know it will be an integer value, then don't use TryGetInt64 or TryGetInt32, instead check ValueKind == JsonValueKind.Null first, and then use GetInt64 or GetInt32.
null, non-integer values will cause a runtime exception (which you should want, as it's an unexpected exceptional circumstance) instead of incorrectly assuming that if TryGetInt32 returns false then the JsonElement "must" be null, which is wrong.TryGetDecimal is okay, I guess.Here's a program I wrote to compare how the different methods handle different types of source JSON number values:
Test( @"{ ""value"": 0 }" );
Test( @"{ ""value"": -1 }" ); // Negative signed integer.
Test( @"{ ""value"": 512 }" );
Test( @"{ ""value"": 1.005 }" ); // Non-integer value.
Test( @"{ ""value"": 9007199254740992 }" ); // `Number.MAX_SAFE_INTEGER + 1`
Test( @"{ ""value"": 3.7e-5 }" ); // Small fractional number.
Test( @"{ ""value"": 9.99e300 }" ); // Outside the range of Decimal and Single, but within Double's range.
static void Test( String json )
{
JsonDocument doc = JsonDocument.Parse( json );
JsonElement valueProp = doc.RootElement.GetProperty("value");
valueProp.Dump();
List<( String method, Boolean ok, Object? value )> list = new();
// Decimal:
list.Add( ( nameof(valueProp.TryGetDecimal), ok: valueProp.TryGetDecimal( out Decimal dec ), value: dec ) );
// IEEE-754:
list.Add( ( nameof(valueProp.TryGetDouble), ok: valueProp.TryGetDouble( out Double dbl ), value: dbl ) );
list.Add( ( nameof(valueProp.TryGetSingle), ok: valueProp.TryGetSingle( out Single sng ), value: sng ) );
// Unsigned integers:
list.Add( ( nameof(valueProp.TryGetUInt64), ok: valueProp.TryGetUInt64( out UInt64 u64 ), value: u64 ) );
list.Add( ( nameof(valueProp.TryGetUInt32), ok: valueProp.TryGetUInt32( out UInt32 u32 ), value: u32 ) );
list.Add( ( nameof(valueProp.TryGetUInt16), ok: valueProp.TryGetUInt16( out UInt16 u16 ), value: u16 ) );
list.Add( ( nameof(valueProp.TryGetByte), ok: valueProp.TryGetByte ( out Byte u8 ), value: u8 ) );
// Signed integers:
list.Add( ( nameof(valueProp.TryGetInt64), ok: valueProp.TryGetInt64( out Int64 s64 ), value: s64 ) );
list.Add( ( nameof(valueProp.TryGetInt32), ok: valueProp.TryGetInt32( out Int32 s32 ), value: s32 ) );
list.Add( ( nameof(valueProp.TryGetInt16), ok: valueProp.TryGetInt16( out Int16 s16 ), value: s16 ) );
list.Add( ( nameof(valueProp.TryGetSByte), ok: valueProp.TryGetSByte( out SByte s8 ), value: s8 ) );
list.Dump();
}
which gives me these results:
| Input JSON | TryGetDecimal |
TryGetDouble |
TryGetSingle |
TryGetUInt64 |
TryGetUInt32 |
TryGetUInt16 |
TryGetByte |
TryGetInt64 |
TryGetInt32 |
TryGetInt16 |
TryGetSByte |
|---|---|---|---|---|---|---|---|---|---|---|---|
{ "value": 0 } |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
{ "value": -1 } |
-1 | -1 | -1 | -1 | -1 | -1 | -1 | ||||
{ "value": 512 } |
512 | 512 | 512 | 512 | 512 | 512 | 512 | 512 | 512 | ||
{ "value": 1.005 } |
1.005 | 1.005 | 1.005 | ||||||||
{ "value": 9007199254740992 } |
9007199254740992 | 9007199254740992 | 9.007199E+15 | 9007199254740992 | 9007199254740992 | ||||||
{ "value": 3.7e-5 } |
0.000037 | 3.7E-05 | 3.7E-05 | ||||||||
{ "value": 9.99e300 } |
9.99E+300 | ∞ |
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With