Does JsonStringEnumConverter (System.Text.Json) support null values?
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!
It is now supported in 5.0 - Honor converters for underlying types of Nullable<T> specified with JsonConverterAttribute.
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);
}
}
}
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.