Search code examples
c#jsonsystem.text.jsonjsonconverter

In a custom JsonConverter, how can I determine whether a Utf8JsonReader numeric token is a decimal or a long?


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;
}

Solution

  • 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.