I have a .NET 6 program that needs to deserialize a JSON string value (returned by external API) into a .NET enum.
The issue is that there are over 100 possible enum values (and more could be added without my knowledge), but I'm only interested in a few of them. So I would like to define an enum type like this and deserialize all the values that I'm not interested in to MyEnum.Unknown
:
public enum MyEnum
{
Unknown = 0,
Value1,
Value2,
Value3,
// only values that I'm interested in will be defined
}
If I'm using Newtonsoft.Json, I can do this quite easily with a custom JSON converter:
public class DefaultFallbackStringEnumConverter : StringEnumConverter
{
private readonly object _defaultValue;
public DefaultFallbackStringEnumConverter() : this(0)
{
}
public DefaultFallbackStringEnumConverter(object defaultValue)
{
_defaultValue = defaultValue;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
return base.ReadJson(reader, objectType, existingValue, serializer);
}
catch (JsonException)
{
return _defaultValue;
}
}
}
But with System.Text.Json, I can't figure out how this can be done easily, because the JsonStringEnumConverter
in STJ is actually a JsonConverterFactory
that doesn't do the serialization itself (it throws exceptions in all overrides of JsonConverter
as you can see here). Instead the factory creates EnumConverter<T>
s that actually do the work, but EnumConverter<T>
is internal so I can't even reference or inherit from it in user code.
Any idea how this can be done easily with STJ or is it not possible at all? Thanks a lot for the help!
You could use the decorator pattern and wrap the JsonStringEnumConverter
factory in a decorator whose CreateConverter()
method wraps the returned EnumConverter<T>
in some inner decorator that catches the exception and returns a default value.
The following does that:
public class DefaultFallbackStringEnumConverter : JsonConverterFactoryDecorator
{
public DefaultFallbackStringEnumConverter(JsonStringEnumConverter inner) : base(inner) { }
public DefaultFallbackStringEnumConverter() : this(new JsonStringEnumConverter()) { }
protected virtual T GetDefaultValue<T>() where T : struct, Enum => default(T);
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var inner = base.CreateConverter(typeToConvert, options);
return (JsonConverter?)Activator.CreateInstance(typeof(EnumConverterDecorator<>).MakeGenericType(Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert), new object? [] { this, inner });
}
sealed class EnumConverterDecorator<T> : JsonConverter<T> where T : struct, Enum
{
readonly DefaultFallbackStringEnumConverter parent;
readonly JsonConverter<T> inner;
public EnumConverterDecorator(DefaultFallbackStringEnumConverter parent, JsonConverter inner) =>
(this.parent, this.inner)= (parent ?? throw new ArgumentException(nameof(parent)), (inner as JsonConverter<T>) ?? throw new ArgumentException(nameof(inner)));
public override bool CanConvert(Type typeToConvert) => inner.CanConvert(typeToConvert);
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
try
{
return inner.Read(ref reader, typeToConvert, options);
}
catch (JsonException)
{
return parent.GetDefaultValue<T>();
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => inner.Write(writer, value, options);
}
}
public class JsonConverterFactoryDecorator : JsonConverterFactory
{
readonly JsonConverterFactory inner;
public JsonConverterFactoryDecorator(JsonConverterFactory inner) => this.inner = inner ?? throw new ArgumentNullException(nameof(inner));
public override bool CanConvert(Type typeToConvert) => inner.CanConvert(typeToConvert);
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => inner.CreateConverter(typeToConvert, options);
}
Do note that, unlike Json.NET, System.Text.Json does not have an equivalent to Json.NET's ConverterParameters
(see issue #54187 for confirmation) so if you need a different default value for a specific enum, you will need to subclass DefaultFallbackStringEnumConverter
for that specific enum, e.g. like so:
public enum MyEnum2
{
Unknown1 = 1, // Use this value for unknown values
Value2 = 2,
Value3 = 3,
}
public class MyEnum2Converter : DefaultFallbackStringEnumConverter
{
protected override T GetDefaultValue<T>() => typeof(T) == typeof(MyEnum2) ? (T)(object)MyEnum2.Unknown1 : base.GetDefaultValue<T>();
public override bool CanConvert(Type typeToConvert) => base.CanConvert(typeToConvert) && typeToConvert == typeof(MyEnum2);
}
Then if your model looks like e.g.:
public record Model(MyEnum MyEnum,
[property: JsonConverter(typeof(MyEnum2Converter))] MyEnum2? MyEnum2);
And your JSON looks like:
{"MyEnum" : "missing value", "MyEnum2" : "missing value" }
You will be able to deserialize and re-serialize as follows:
var options = new JsonSerializerOptions
{
Converters = { new DefaultFallbackStringEnumConverter() },
};
var model = JsonSerializer.Deserialize<Model>(json, options);
var newJson = JsonSerializer.Serialize(model, options);
Demo fiddle here.