Home > other >  Why is Utf8JsonReader.TokenType required with JsonConvert<int>, but not JsonConvert<double&
Why is Utf8JsonReader.TokenType required with JsonConvert<int>, but not JsonConvert<double&

Time:10-09

In order to be able to deserialize the following JSON:

[
    {
        "name": "Luke Skywalker",
        "height": "172",
        "mass": "77",
        "birth_year": "19BBY",
        "gender": "male"
    }
]

into the following record:

record class Character
{
    public string Name { get; init; } = "";

    [property: JsonPropertyName("birth_year")]
    public double Birth { get; init; }

    public int Height { get; init; }

    [property: JsonPropertyName("mass")]
    public int Weight { get; init; }

    public GenderEnum Gender { get; init; }
}

I use the following JsonSerializerOptions:

var options = new JsonSerializerOptions()
{
    PropertyNameCaseInsensitive = true,
    NumberHandling = JsonNumberHandling.AllowReadingFromString,
    Converters =
    {
        new GenderEnumConverter(),
        new BirthYearConverter(),
        new MeasurementsConverter()
    }
};

Problem:

Two of these Converters handle string-to-double and string-to-int deserialization, respectively:

class BirthYearConverter : JsonConverter<double>
{
    public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return double.TryParse(reader.GetString()?.TrimEnd('B', 'b', 'Y', 'y'), out double result) ? result : 0;
    }
    
    // ...
}

class MeasurementsConverter : JsonConverter<int>
{
    public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.String && options.NumberHandling == JsonNumberHandling.AllowReadingFromString)
        {
            return int.TryParse(reader.GetString(), out int result) ? result : -1;
        }

        return reader.GetInt32();
    }

    // ...
}

These converters are necessary. If you are not familiar with swapi.dev/api, the numeric values are presented as strings and can include invalid integer or floating point property values.

With the above code everything runs as expected.

But the odd thing to observe here is that the int converter can't be simplified like the double one. I am forced to check for the TokenType. If I don't do that and try a similar approach to the double converter and use int.TryParse(reader.GetString()... I will get the following exception:

System.InvalidOperationException: 'Cannot get the value of a token type 'String' as a number.'

So my question is, why was I forced to explicitly handle strings in the JsonConverter<int> converter to avoid an InvalidOperationException, but that wasn't required of me when using JsonConverter<double> where I was able to make use of reader.GetString()?

Note: For additional information, you can refer to the separate question that originated this one.

CodePudding user response:

MeasurementsConverter seems to be kind of pointless. Specifying NumberHandling = JsonNumberHandling.AllowReadingFromString should be enough (at least for sample JSON):

var options = new JsonSerializerOptions()
{
    PropertyNameCaseInsensitive = true,
    NumberHandling = JsonNumberHandling.AllowReadingFromString,
    Converters =
    {
        new GenderEnumConverter(),
        new BirthYearConverter()
    }
};

As for the question - you would be required to do the same in JsonConverter<double> because reader.GetDouble() will throw as reader.GetInt32() did, you just don't call it because you need to sanitize the string so you go straight to the reader.GetString(). I.e. complete analog of your JsonConverter<double> approach would look like:

class MeasurementsConverter : JsonConverter<int>
{
    public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return int.TryParse(reader.GetString(), out int result) ? result : -1;
    }
    // ...
}

In current implementation Utf8JsonReader.GetString() always throws if the token type is not a valid one. So if you provide both double and int values as numbers in the source JSON, they both will fail with the same error (without explicit handling for JsonTokenType.String). I.e.:

[
    {
        "name": "Luke Skywalker",
        "height": "172",
        "mass": 77,
        "birth_year": "19BBY",
        "gender": "male"
    }
]

will fail with the same error ("Cannot get the value of a token type 'Number' as a string.") as:

[
    {
        "name": "Luke Skywalker",
        "height": "172",
        "mass": "77",
        "birth_year": 19,
        "gender": "male"
    }
]
  • Related