using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json.Nodes;
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)
return inner.Read(ref reader, typeToConvert, options);
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);
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);
public record Model(MyEnum MyEnum,
[property: JsonConverter(typeof(MyEnum2Converter))] MyEnum2? MyEnum2);
public static void Test()
{"MyEnum" : "missing value", "MyEnum2" : "missing value" }
var options = new JsonSerializerOptions
Converters = { new DefaultFallbackStringEnumConverter() },
var model = JsonSerializer.Deserialize<Model>(json, options);
var newJson = JsonSerializer.Serialize(model, options);
Console.WriteLine(newJson);
static void TestTruncated(string json)
var options = new JsonSerializerOptions
Converters = { new DefaultFallbackStringEnumConverter() },
Assert.DoesNotThrow(() => JsonSerializer.Deserialize<Model>(json, options));
for (int length = json.Length - 1; length >= 0; length--)
Assert.That(() => JsonSerializer.Deserialize<Model>(json.Substring(0, length), options), Throws.Exception);
public static class ObjectExtensions
public static T ThrowOnNull<T>(this T? value) where T : class => value ?? throw new ArgumentNullException();
public static void Main()
Console.WriteLine("Environment version: {0} ({1}), {2}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , Environment.Version, Environment.OSVersion);
Console.WriteLine("System.Text.Json version: " + typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine("Failed with unhandled exception: ");