0

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
Alexandre Bell
  • 3,141
  • 3
  • 30
  • 43
  • Please share the stacktrace of excpetion – Amir Oct 08 '22 at 20:55
  • @Amir That isn't required. I updated the original question to include additional information. – Alexandre Bell Oct 08 '22 at 21:15
  • Just see this dude ;-) https://source.dot.net/#System.Text.Json/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs,02b55254c0b3b478,references. you can the go through TryGetInt32 and see the error ;-) – Amir Oct 08 '22 at 21:29

1 Answers1

1

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"
    }
]
Timothy G.
  • 6,335
  • 7
  • 30
  • 46
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • This question had its origin here: https://stackoverflow.com/questions/73998857/cannot-deserialize-a-string-to-an-int-despite-having-a-converter-and-jsonnumber?noredirect=1#comment130657441_73998857. Unfortunately, Setting `NumberHandling` to read from strings is not enough. – Alexandre Bell Oct 08 '22 at 21:03
  • @AlexandreBell can you please explain what I'm missing - cause it [works for me](https://dotnetfiddle.net/0qsWjm) – Guru Stron Oct 08 '22 at 21:08
  • @AlexandreBell see the update. The only difference it seems that you are not getting numer values for doubles, i.e. in source json `birth_year` is always actually a string. – Guru Stron Oct 08 '22 at 21:15
  • I don't know what happened. But you are right. It does work. I don't know why it wasn't before. What could I have been doing that would lead me to that exception and that now it isn't. Seems like a waste of everyone's time... apologies. – Alexandre Bell Oct 08 '22 at 21:23
  • @AlexandreBell NP, was glad to help! And again - it can be fluctuation in the source data. The `JsonNumberHandling.AllowReadingFromString` for sure will not parse non-number strings. – Guru Stron Oct 08 '22 at 21:25