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;
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() {}
public override bool CanConvert(Type typeToConvert) {
var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) {
return new CustomStringEnumConverter(options);
private class CustomStringEnumConverter : JsonConverter<Enum?>
public CustomStringEnumConverter(JsonSerializerOptions options) { }
public override Enum? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
var isNullable = Nullable.GetUnderlyingType(typeToConvert) != null;
var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
switch (reader.TokenType) {
case JsonTokenType.Null when !isNullable:
throw new JsonException("Cannot deserialise null value to non-nullable field");
case JsonTokenType.String:
var result = ReadStringValue(reader, enumType);
case JsonTokenType.Number:
return ReadNumberValue(reader, enumType);
public override void Write(Utf8JsonWriter writer, Enum? value, JsonSerializerOptions options) {
var description = value.ToString();
writer.WriteStringValue(description);
public override bool CanConvert(Type typeToConvert) {
var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
private static string GetDescription(Enum source) {
var fieldInfo = source.GetType().GetField(source.ToString());
return source.ToString();
var attributes = (System.ComponentModel.DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(System.ComponentModel.DescriptionAttribute), false);
return attributes != null && attributes.Length > 0
? attributes[0].Description
private static object? ReadStringValue(Utf8JsonReader reader, Type enumType) {
var parsedValue = reader.GetString()!;
foreach (var item in Enum.GetValues(enumType))
var attribute = item.GetType().GetTypeInfo().GetRuntimeField(item.ToString()).GetCustomAttribute<AlternativeValueAttribute>();
if (attribute == null && Enum.TryParse(enumType, parsedValue, true, out var result)) {
if (attribute != null && attribute.Code == parsedValue &&
Enum.TryParse(enumType, item.ToString(), true, out var attributedResult)) {
if (parsedValue == item.ToString() && Enum.TryParse(enumType, parsedValue, true, out var parsedResult)) {
private static Enum? ReadNumberValue(Utf8JsonReader reader, Type enumType) {
var result = int.Parse(reader.GetString()!);
var castResult = Enum.ToObject(enumType, result);
foreach (var item in Enum.GetValues(enumType)) {
if (castResult.Equals(item)) {
return (Enum?)Convert.ChangeType(castResult, enumType);
throw new JsonException($"Could not convert '{result}' to enum of type '{enumType.Name}'.");
[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 static void Test()
var converter = new AlternativeValueJsonStringEnumConverter();
var inner = converter.CreateConverter(typeof(Allowances), JsonSerializerOptions.Default);
Console.WriteLine("{0}.CanConvert(typeof(Allowances))={1}", inner, inner!.CanConvert(typeof(Allowances)));
var typeToConvert = (Type?)inner.GetType().GetProperty("TypeToConvert", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).GetValue(inner);
Console.WriteLine("typeToConvert={0}", typeToConvert);
new WhenUsingCustomSerialiser().ItShouldDeserialiseWhenValueIsDecorated();
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 }", settings);
output.Should().BeEquivalentTo(new { Type = Allowances.Car, Value = 25.75M });
var result = JsonSerializer.Serialize(output, settings);
Console.WriteLine(result);
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: ");