Parsing JSON with variable field types

October 11th 2024 .NET Serialization

JSON serialization allows a lot of flexibility, sometimes even too much, e.g., a field that can be serialized into two different JSON types, depending on its contents. It could be serialized as string:

{
  "Version": "1.0.0"
}

Or, it could be serialized as a number:

{
  "Version": 1
}

I would be perfectly happy if the value was deserialized as a string in my code:

internal class AppInfo
{
    public string? Version { get; set; }
}

However, with System.Text.Json.JsonSerializer, this only worked when value was serialized as a string. When it was serialized as a number, it threw an exception:

System.Text.Json.JsonException : The JSON value could not be converted to System.String.

To make it work, I had to implement a custom JsonConverter, which can deserialize both a string and a number value:

internal class NumericStringAsNumberJsonConverter : JsonConverter<string>
{
    public override string? Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options
    )
    {
        return reader.TokenType switch
        {
            JsonTokenType.Number => reader.GetInt64().ToString(),
            JsonTokenType.String => reader.GetString(),
            _ => throw new JsonException(
                $"The JSON value could not be converted to {typeToConvert}."
            ),
        };
    }

    public override void Write(
        Utf8JsonWriter writer,
        string value,
        JsonSerializerOptions options
    )
    {
        if (long.TryParse(value, out var number))
        {
            writer.WriteNumberValue(number);
        }
        else
        {
            writer.WriteStringValue(value);
        }
    }
}

The Read method was all I needed, but I decided to also implement the Write method in case I ever had to serialize the values. I applied the converter to the specific JSON field using the JsonConverter attribute:

internal class AppInfo
{
    [JsonConverter(typeof(NumericStringAsNumberJsonConverter))]
    public string? Version { get; set; }
}

You can find a sample project with the custom converter in my GitHub repository. It also contains some unit tests to showcase that both deserialization and serialization work correctly. The second to last commit demonstrates the default deserialization behavior without a custom converter.

This blog post shows how a custom JsonConverter can be assigned to a property to change the serialization behavior for a single JSON field. It's a customization feature I haven't used often, but it can come in very handy when you need it.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License