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.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json.Nodes;
using FluentAssertions.Execution;
using System.Diagnostics;
converter = options.ExpandConverterFactory(converter, typeToConvert);
if (!converter.TypeToConvert.IsInSubtypeRelationshipWith(typeToConvert))
ThrowHelper.ThrowInvalidOperationException_SerializationConverterNotCompatible(converter.GetType(), typeToConvert);
[AttributeUsage(AttributeTargets.Field)]
public class AlternativeValueAttribute : Attribute
public AlternativeValueAttribute(string code) {
public string Code { get; }
public class AlternativeValueJsonStringEnumConverter : JsonConverterFactory {
public AlternativeValueJsonStringEnumConverter() {}
static readonly JsonConverterFactory defaultConverter = new JsonStringEnumConverter(namingPolicy : null, allowIntegerValues : true);
public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum;
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) {
Debug.Assert(typeToConvert.IsEnum);
var innerDefaultConverter = defaultConverter.CreateConverter(typeToConvert, options);
var values = Enum.GetValues(typeToConvert).Cast<Enum>()
.Select(item => (Item : item, Code : item.GetType().GetTypeInfo().GetRuntimeField(item.ToString())?.GetCustomAttribute<AlternativeValueAttribute>()?.Code))
.Where(p => p.Code != null)
return (JsonConverter?)Activator.CreateInstance(typeof(CustomStringEnumConverter<>).MakeGenericType(typeToConvert), new object? [] { options, values, innerDefaultConverter });
return innerDefaultConverter;
private class CustomStringEnumConverter<TEnum> : JsonConverter<TEnum> where TEnum : struct, Enum
readonly Dictionary<string, TEnum> codes;
readonly JsonConverter<TEnum> innerDefaultConverter;
public CustomStringEnumConverter(JsonSerializerOptions options, List<(Enum Item, string Code)> values, JsonConverter innerDefaultConverter)
this.codes = values.ToDictionary(p => p.Code, p => (TEnum)p.Item);
this.innerDefaultConverter = (JsonConverter<TEnum>)innerDefaultConverter ?? throw new ArgumentNullException(nameof(innerDefaultConverter));
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
TryReadAlternateValue(ref reader, out var value) ? value : innerDefaultConverter.Read(ref reader, typeToConvert, options);
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) =>
innerDefaultConverter.Write(writer, value, options);
public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum;
private bool TryReadAlternateValue(ref Utf8JsonReader reader, out TEnum value)
if (reader.TokenType == JsonTokenType.String && codes.TryGetValue(reader.GetString()!, out value))
[AlternativeValue("NCD")] NegativeCar = -10,
[AlternativeValue("CD")] Car = 0,
[AlternativeValue("AD")] Transport = 1,
[AlternativeValue("LD")] Laundry = 2
public class AllowanceRequest
public Allowances Type { get; set; }
public Allowances? AdditionalType { get; set; }
public decimal Value { get; set; }
public FlagLongEnum? Flags { get; set; }
public enum FlagLongEnum : long
FlagZero = ((long)1 << 0),
FlagOne = ((long)1 << 1),
FlagTwo = ((long)1 << 2),
FlagSeven = ((long)1 << 7),
FlagFourteen = ((long)1 << 14),
FlagThirtyOne = ((long)1 << 31),
FlagThirtyTwo = ((long)1 << 32),
FlagSixtyTwo = ((long)1 << 62),
FlagSixtyThree = ((long)1 << 63),
public static void Test()
var converter = new AlternativeValueJsonStringEnumConverter();
new WhenUsingCustomSerialiser().ItShouldDeserialiseWhenValueIsDecorated();
new WhenUsingCustomSerialiser().ItShouldDeserialiseWhenValueIsNumeric();
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture =
new CultureInfo("sv-SE");
new WhenUsingCustomSerialiser().ItShouldDeserialiseWhenValueIsNumeric();
public class WhenUsingCustomSerialiser
public void ItShouldDeserialiseWhenValueIsDecorated()
var settings = new JsonSerializerOptions { WriteIndented = false };
settings.Converters.Add(new AlternativeValueJsonStringEnumConverter());
var output = JsonSerializer.Deserialize<AllowanceRequest>("{ \"Type\": \"CD\", \"Value\": 25.75, \"Flags\" : \"FlagOne, FlagThirtyTwo, FlagSixtyTwo\"}", settings);
output.Should().BeEquivalentTo(new { Type = Allowances.Car, Value = 25.75M, Flags = FlagLongEnum.FlagOne | FlagLongEnum.FlagThirtyTwo | FlagLongEnum.FlagSixtyTwo });
var result = JsonSerializer.Serialize(output, settings);
Console.WriteLine(result);
public void ItShouldDeserialiseWhenValueIsNumeric()
var settings = new JsonSerializerOptions { WriteIndented = false };
settings.Converters.Add(new AlternativeValueJsonStringEnumConverter());
var output = JsonSerializer.Deserialize<AllowanceRequest>("{ \"Type\": -10, \"Value\": 25.75, \"Flags\" : \"FlagOne, FlagThirtyTwo, FlagSixtyTwo\"}", settings);
output.Should().BeEquivalentTo(new { Type = Allowances.NegativeCar, Value = 25.75M, Flags = FlagLongEnum.FlagOne | FlagLongEnum.FlagThirtyTwo | FlagLongEnum.FlagSixtyTwo });
var result = JsonSerializer.Serialize(output, settings);
Console.WriteLine(result);
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: ");