1

I am trying to deserialize the following JSON (an array with a single entry):

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

Into this record:

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

With the following JsonSerializerOptions setup:

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

The two top converters work well. It's when I add the MeasurementsConverter that I get an exception:

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

    public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString());
    }
}

The exception is thrown on the Read method:

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

When I deserialize with the following two expressions:

string result = await File.ReadAllTextAsync("people.json");
List<Character> people = JsonSerializer.Deserialize<List<Character>>(result, options);

Can you help me understand why the MeasurementsConverter : JsonConverter<int> is ignoring the NumberHandling = JsonNumberHandling.AllowReadingFromString option?

Note: This json is a sample from swapi.dev/api/people. If you know the API, at least one entry in the resulting array will have "unknown" as a value for the weight attribute. Hence, this converter.

dbc
  • 104,963
  • 20
  • 228
  • 340
Alexandre Bell
  • 3,141
  • 3
  • 30
  • 43
  • 2
    I wouldn't be at all surprised if most of the JsonSerializerOptions around how values are handled (once the appropriate property has been identified) only applied to the default converter. After all `Utf8JsonReader` is lower level, and doesn't have all those options. I can't see that explicitly specified in the docs, but I think it would make sense for you to just make your `MeasurementsConverter` explicitly handle string as well. (I expect you can use the TokenType property to work out what to do.) – Jon Skeet Oct 08 '22 at 16:55
  • This raises other questions -- one of those Converters is a double converter, and I didn't have the need to do this -- But I'll leave that to a separate post. – Alexandre Bell Oct 08 '22 at 18:27

1 Answers1

1

From Jon's comment above, I was able to avoid triggering the exception by making explicit use of the Utf8JsonReader.TokenType property of the reader object.

The Read() override now looks like this:

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();
} 
Alexandre Bell
  • 3,141
  • 3
  • 30
  • 43