Does JsonStringEnumConverter (System.Text.Json) support null values?
Asked Answered
S

6

25

I am shifting my code from .NET Core 2.x to .NET Core 3.x (i.e. use the native library System.Text.Json). In doing this, I ran into some issues with how the former Newtonsoft.Json support for nullable enums does not have a clear migration path at the moment --- it looks like it is not supported in .NET Core 3.x?.

For example, using Newtonsoft.Json, the JSON converter supported nullable enums, like so:

public enum UserStatus
{
    NotConfirmed,
    Active,
    Deleted
}

public class User
{
    public string UserName { get; set; }

    [JsonConverter(typeof(StringEnumConverter))]  // using Newtonsoft.Json
    public UserStatus? Status { get; set; }       // Nullable Enum
}

The current version of the native library System.Text.Json, does not seem to support this.

How do I solve this problem? I cannot migrate my code!

Supposal answered 17/12, 2019 at 18:11 Comment(2)
Native support for nullable enum support in JsonStringEnumConverter is being tracked at github.com/dotnet/corefx/issues/41307.Shiprigged
@NitinAgarwal Let's hope it gets implemented soon!Supposal
S
18

Unfortunately, there is currently no support "out-of-the-box" in System.Text.Json to convert nullable enums.

However, there is a solution by using your own custom converter. (see below).


The solution. Use a custom converter.

You would attach can attach it to your property by decorating it with the custom converter:

// using System.Text.Json
[JsonConverter(typeof(StringNullableEnumConverter<UserStatus?>))]  // Note the '?'
public UserStatus? Status { get; set; }                            // Nullable Enum

Here is the converter:

public class StringNullableEnumConverter<T> : JsonConverter<T>
{
    private readonly JsonConverter<T> _converter;
    private readonly Type _underlyingType;

    public StringNullableEnumConverter() : this(null) { }

    public StringNullableEnumConverter(JsonSerializerOptions options)
    {
        // for performance, use the existing converter if available
        if (options != null)
        {
            _converter = (JsonConverter<T>)options.GetConverter(typeof(T));
        }

        // cache the underlying type
        _underlyingType = Nullable.GetUnderlyingType(typeof(T));
    }

    public override bool CanConvert(Type typeToConvert)
    {
        return typeof(T).IsAssignableFrom(typeToConvert);
    }

    public override T Read(ref Utf8JsonReader reader, 
        Type typeToConvert, JsonSerializerOptions options)
    {
        if (_converter != null)
        {
            return _converter.Read(ref reader, _underlyingType, options);
        }

        string value = reader.GetString();

        if (String.IsNullOrEmpty(value)) return default;

        // for performance, parse with ignoreCase:false first.
        if (!Enum.TryParse(_underlyingType, value, 
            ignoreCase: false, out object result) 
        && !Enum.TryParse(_underlyingType, value, 
            ignoreCase: true, out result))
        {
            throw new JsonException(
                $"Unable to convert \"{value}\" to Enum \"{_underlyingType}\".");
        }

        return (T)result;
    }

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

Hope that helps until there is native support without the need for a custom converter!

Supposal answered 17/12, 2019 at 18:11 Comment(0)
S
11

It is now supported in 5.0 - Honor converters for underlying types of Nullable<T> specified with JsonConverterAttribute.

Simard answered 16/4, 2021 at 14:42 Comment(0)
S
11

For Dotnet 6, this works out of the box:

public enum UserStatus
{
    NotConfirmed,
    Active,
    Deleted
}

public class User
{
    public string UserName { get; set; }

    [JsonConverter(typeof(JsonStringEnumConverter))]
    public UserStatus? Status { get; set; }
}

Try it on Dotnet fiddle

See official documentation

Selena answered 31/8, 2022 at 19:33 Comment(0)
H
4

I found Svek's answer very helpful, however I wanted to have the converter compatible with nullable as well as non-nullable enum properties.

I accomplished this by tweaking his converter as follows:

public class JsonNullableEnumStringConverter<TEnum> : JsonConverter<TEnum>
{
    private readonly bool _isNullable;
    private readonly Type _enumType;

    public JsonNullableEnumStringConverter() {
        _isNullable = Nullable.GetUnderlyingType(typeof(TEnum)) != null;

        // cache the underlying type
        _enumType = _isNullable ? 
            Nullable.GetUnderlyingType(typeof(TEnum)) : 
            typeof(TEnum);
    }

    public override TEnum Read(ref Utf8JsonReader reader,
        Type typeToConvert, JsonSerializerOptions options)
    {
        var value = reader.GetString();

        if (_isNullable && string.IsNullOrEmpty(value))
            return default; //It's a nullable enum, so this returns null. 
        else if (string.IsNullOrEmpty(value))
            throw new InvalidEnumArgumentException(
                $"A value must be provided for non-nullable enum property of type {typeof(TEnum).FullName}");

        // for performance, parse with ignoreCase:false first.
        if (!Enum.TryParse(_enumType, value, false, out var result)
            && !Enum.TryParse(_enumType, value, true, out result))
        {
            throw new JsonException(
                $"Unable to convert \"{value}\" to Enum \"{_enumType}\".");
        }

        return (TEnum)result;
    }

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

I also left out some elements that I didn't need in my solution. Hope this is helpful to someone out there.

Herbarium answered 16/2, 2021 at 16:5 Comment(0)
G
2

Another option is to configure support for nullable enums via options:

        JsonSerializerOptions JsonOptions = new()
        {
            Converters =
            {
                new JsonNullableStringEnumConverter(),
            },
        };

Source for JsonNullableStringEnumConverter is following:

#nullable enable

    public class JsonNullableStringEnumConverter : JsonConverterFactory
    {
        readonly JsonStringEnumConverter stringEnumConverter;

        public JsonNullableStringEnumConverter(JsonNamingPolicy? namingPolicy = null, bool allowIntegerValues = true)
        {
            stringEnumConverter = new(namingPolicy, allowIntegerValues);
        }

        public override bool CanConvert(Type typeToConvert)
            => Nullable.GetUnderlyingType(typeToConvert)?.IsEnum == true;

        public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
        {
            var type = Nullable.GetUnderlyingType(typeToConvert)!;
            return (JsonConverter?)Activator.CreateInstance(typeof(ValueConverter<>).MakeGenericType(type),
                stringEnumConverter.CreateConverter(type, options));
        }

        class ValueConverter<T> : JsonConverter<T?>
            where T : struct, Enum
        {
            readonly JsonConverter<T> converter;

            public ValueConverter(JsonConverter<T> converter)
            {
                this.converter = converter;
            }

            public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            {
                if (reader.TokenType == JsonTokenType.Null)
                {
                    reader.Read();
                    return null;
                }
                return converter.Read(ref reader, typeof(T), options);
            }

            public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
            {
                if (value == null)
                    writer.WriteNullValue();
                else
                    converter.Write(writer, value.Value, options);
            }
        }
    }
Gonnella answered 29/3, 2021 at 11:4 Comment(0)
O
-1

You should be able to get back your original behavior by installing Newtonsoft JSON nuget and placing this in your code, I suppose you are migrating an ASP app:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
        .AddNewtonsoftJson();
}
Oscillograph answered 31/12, 2019 at 13:37 Comment(2)
The idea is to use the newer, "better" (performance and Microsoft compatibility in the long term) System.Text.Json that came with ASP.NET Core 3.x. --- The "migration" mentioned was from 2.x to 3.xSupposal
@Supposal I can relate to that, however, all new shiny Json Core functionality has some gaps, so for time being out team decided to rather use this approach, which I hope can be helpful to some other people too as it answers your question in its original form - "how to solve this problem?".Oscillograph

© 2022 - 2024 — McMap. All rights reserved.