2

I have this jsonconverter which needs to convert a given property value either to a decimal or a long, depending on the value - but I can't seem to determine when the propertyvalue is decimal or long, since the tokentype only can detect number... how do I resolve this issue?

public override IDictionary<string, object> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    IDictionary<string, object> output = new Dictionary<string, object>();

    while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
    {
        string propertyName = reader.GetString();
        reader.Read();
        object? propertyValue = null;

        switch (reader.TokenType)
        {
            case JsonTokenType.Number:
                propertyValue = reader.GetInt64();  // or could be a decimal for where I should reader.GetDecimal()
                break;
            case JsonTokenType.String:
                if (reader.TryGetDateTime(out DateTime value))
                {
                    propertyValue = value;
                }
                else
                {
                    propertyValue = reader.GetString();
                }

                break;
            case JsonTokenType.True:
            case JsonTokenType.False:
                propertyValue = reader.GetBoolean();
                break;
        }

        output.Add(propertyName, propertyValue);
    }

    return output;
}
dbc
  • 104,963
  • 20
  • 228
  • 340
kafka
  • 573
  • 1
  • 11
  • 28
  • All JavaScript numbers are actually `double`s under the cover. There is no further subtyping for numerical values (which is why, if you need to represent exact numbers, you're typically better off packing them in strings). You can try to apply heuristics as to what the most "correct" type should be for ultimate conversion, but you're typically better off just fixing a type based on the actual use case. If you don't know, the best you can do is use `double` and pass the responsibility. – Jeroen Mostert Mar 28 '20 at 22:42
  • hmm... problem is that this is being parsed to an sql function, and having the property value stored in the correct type is quite crucial.. – kafka Mar 28 '20 at 22:53
  • Then convert it to the correct type. Just because JavaScript only has `double`s doesn't mean the value ultimately has to be treated that way -- just that you can't infer the type from the value. It's not like this is even unique to JavaScript -- SQL Server's `DECIMAL` type doesn't correspond exactly to C#'s `decimal` either, for example (the former has a fixed scale and precision, the latter does not). – Jeroen Mostert Mar 29 '20 at 16:02
  • @JeroenMostert - I stored it as a string, and converted it to a to a long from a string – kafka Mar 30 '20 at 08:03

1 Answers1

3

You can use Utf8JsonReader.TryGetInt64(out long value) and Utf8JsonReader.TryGetDecimal(out decimal value) to test to see whether the current value can be successfully parsed as a long or a decimal.

Thus your modified Read() method should look like:

public override IDictionary<string, object> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    // Assert we are currently at the beginning of an object
    if (reader.TokenType != JsonTokenType.StartObject)
        throw new JsonException(string.Format("Unexpected token {0}", reader.TokenType));

    IDictionary<string, object> output = new Dictionary<string, object>();

    while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
    {
        string propertyName = reader.GetString()!;
        reader.Read();
        object? propertyValue;

        switch (reader.TokenType)
        {
            case JsonTokenType.Number:
                if (reader.TryGetInt64(out var l))
                    propertyValue = l;
                else if (reader.TryGetDecimal(out var d))
                    propertyValue = d;
                else
                {
                    // Either store the value as a string, or throw an exception.
                    using var doc = JsonDocument.ParseValue(ref reader);
                    propertyValue = doc.RootElement.ToString();
                    throw new JsonException(string.Format("Cannot parse number: {0}", propertyValue));
                }
                break;
            case JsonTokenType.String:
                if (reader.TryGetDateTime(out var dt))
                    propertyValue = dt;
                else
                    propertyValue = reader.GetString();
                break;
            case JsonTokenType.True:
            case JsonTokenType.False:
                propertyValue = reader.GetBoolean();
                break;
            case JsonTokenType.Null:
                propertyValue = null;
                break;
            default:
                // An unexpected token type such as an object or array.
                // You must either skip it or throw an exception.
                reader.Skip();
                propertyValue = null;
                throw new JsonException(string.Format("Unexpected token {0}", reader.TokenType));
                //break;
        }
        // Since your converter is of type IDictionary<string, object> I assume you don't want to allow null values.
        // If you do want to allow null values you should declare it as IDictionary<string, object?>
        if (propertyValue == null)
            throw new JsonException("null value");
        output.Add(propertyName, propertyValue);
    }

    return output;
}

Notes:

  • While Utf8JsonReader will throw an exception on malformed JSON, it is the responsibility of the converter to handle any type of valid value token, and throw an exception on any unsupported value type.

    I modified the converter to throw exceptions for unexpected value types as needed.

  • Since you seem to have enabled nullable reference type checking, I modified your code to throw an exception if propertyValue is null. If you want to allow null property values you should declare your converter as a JsonConverter<IDictionary<string, object?>>.

  • Your converter only handles primitive values. If you wanted to extend it to deserialize nested objects to nested Dictionary<string, object> values, and nested arrays to nested List<object> values, you could look at ObjectAsPrimitiveConverter from this answer to C# - Deserializing nested json to nested Dictionary<string, object>.

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340