28

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!

2
  • 2
    Native support for nullable enum support in JsonStringEnumConverter is being tracked at github.com/dotnet/corefx/issues/41307. Commented Jan 31, 2020 at 2:20
  • @NitinAgarwal Let's hope it gets implemented soon! Commented Jan 31, 2020 at 10:31

6 Answers 6

22

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!

Sign up to request clarification or add additional context in comments.

Comments

20

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

Comments

11

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

Comments

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.

Comments

3

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

Comments

-2

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(); } 

2 Comments

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.x
@Svek 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?".

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.