Handling of unknown enums in .NET serialization for API integration

Enumerations, or Enums, serve as a comprehensive method for representing named values, such as currencies (e.g., EUR, USD, NOK) and languages (e.g., English, German, Spanish). They are integral to the vast majority of programming languages. Additionally, OpenAPI (Swagger) accommodates Enums through its utilization of JSON, a subset of JavaScript. However, given that JavaScript was developed in a mere 10 days, the implementation of enums was not incorporated :) Thus, in JSON and JavaScript, an enum is represented as either a string or a numeric type.

Typically, enums remain constant and undergo minimal changes over time. After all, the introduction of a new currency or language is a rare occurrence. However, when such changes do occur, they can result in substantial issues.

Have you ever encountered the errors displayed below? If so, let’s navigate through a solution.

Newtonsoft.Json.JsonSerializationException : Error converting value "Bitcoin" to type 'Gaev.Blog.EnumAsStringTrap.Currency'. Path '[2].Currency', line 4, position 26.
  ----> System.ArgumentException : Requested value 'Bitcoin' was not found.
   at Newtonsoft.Json.Converters.StringEnumConverter.ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.DeserializeConvertable(JsonConverter converter, JsonReader reader, Type objectType, Object existingValue)
System.Text.Json.JsonException : The JSON value could not be converted to Gaev.Blog.EnumAsStringTrap.Money. Path: $[2].Currency | LineNumber: 3 | BytePositionInLine: 26.
   at System.Text.Json.ThrowHelper.ThrowJsonException(String message)
   at System.Text.Json.Serialization.Converters.EnumConverter`1.ReadEnumUsingNamingPolicy(String enumString)
System.InvalidOperationException : There is an error in XML document (4, 41).
  ----> System.InvalidOperationException : Instance validation error: 'Bitcoin' is not a valid value for Currency.
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
   at System.Xml.Serialization.XmlSerializer.Deserialize(Stream stream)

Problem

Consider a scenario where our application integrates with a bank’s API to retrieve transaction details. When the application executes GET api/transactions, the API returns the following response:

[
   {"Currency": "EUR", "Amount": 1},
   {"Currency": "USD", "Amount": 2}
]

For the purpose of deserializing the provided JSON and executing business logic, we have the subsequent models:

public record Money(Currency Currency, decimal Amount);

public enum Currency
{
    Undefined = 0,
    EUR = 1,
    USD = 2
}

Additionally, we have a deserialization method via Newtonsoft.Json:

Money[] Deserialize(string json)
{
    return JsonConvert.DeserializeObject<Money[]>(json, new JsonSerializerSettings
    {
        Converters = new List<JsonConverter> { new StringEnumConverter() }
    });
}

All operates smoothly until the bank incorporates support for a new currency, namely Bitcoin.

[
   {"Currency": "EUR", "Amount": 1},
   {"Currency": "USD", "Amount": 2},
   {"Currency": "Bitcoin", "Amount": 3}
]

Subsequently, deserializing the entire list becomes unfeasible, resulting in the aforementioned exceptions. This disrupts the bank integration process.

Solution

To circumvent such disruptions in the future, several strategies can be employed:

  • Adapt string enums as a string type in the .NET model.
  • Customize .NET JSON/XML serializer to accommodate unknown string enums.

While it would be advantageous to deserialize an enum even when the corresponding value is undefined, libraries such as Newtonsoft.Json, System.Text.Json, and System.Xml.Serialization lack inherent support for this feature. Nevertheless, extensions can readily be implemented.

Newtonsoft.Json

To facilitate unknown enums, the StringEnumConverter can be extended to return the default enum value when a specific error is encountered.

public class UnknownEnumConverter : StringEnumConverter
{
    public override object ReadJson(JsonReader reader, Type enumType, object existingValue, JsonSerializer serializer)
    {
        try
        {
            return base.ReadJson(reader, enumType, existingValue, serializer);
        }
        catch (JsonSerializationException) when (enumType.IsEnum)
        {
            return Activator.CreateInstance(enumType);
        }
    }
}

Here, Activator.CreateInstance(enumType) yields the default value, which for the Currency enum, is Undefined.

[Test]
public void It_should_deserialize_unknown_enum_value()
{
    // Given
    var json = """
               [
                   {"Currency": "EUR", "Amount": 1},
                   {"Currency": "USD", "Amount": 2},
                   {"Currency": "Bitcoin", "Amount": 3}
               ]
               """;

    // When
    var actual = JsonConvert.DeserializeObject<Money[]>(json, new JsonSerializerSettings
    {
        Converters = new List<JsonConverter> { new UnknownEnumConverter() }
    });

    //Then
    actual.Should().BeEquivalentTo(new[]
    {
        new Money(Currency.EUR, 1),
        new Money(Currency.USD, 2),
        new Money(default, 3),
    });
}

System.Text.Json

To support unknown enums in System.Text.Json we should extend built-in JsonStringEnumConverter but the approach is the same as above.

public class UnknownEnumConverter : JsonConverterFactory
{
    private readonly JsonStringEnumConverter _underlying = new();

    public sealed override bool CanConvert(Type enumType)
        => _underlying.CanConvert(enumType);

    public sealed override JsonConverter CreateConverter(Type enumType, JsonSerializerOptions options)
    {
        var underlyingConverter = _underlying.CreateConverter(enumType, options);
        var converterType = typeof(UnknownEnumConverter<>).MakeGenericType(enumType);
        return (JsonConverter)Activator.CreateInstance(converterType, underlyingConverter);
    }
}
public class UnknownEnumConverter<T> : JsonConverter<T> where T : struct, Enum
{
    private readonly JsonConverter<T> _underlying;

    public UnknownEnumConverter(JsonConverter<T> underlying)
        => _underlying = underlying;

    public override T Read(ref Utf8JsonReader reader, Type enumType, JsonSerializerOptions options)
    {
        try
        {
            return _underlying.Read(ref reader, enumType, options);
        }
        catch (JsonException) when (enumType.IsEnum)
        {
            return default;
        }
    }

    public override bool CanConvert(Type typeToConvert)
        => _underlying.CanConvert(typeToConvert);

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
        => _underlying.Write(writer, value, options);

    public override T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => _underlying.ReadAsPropertyName(ref reader, typeToConvert, options);

    public override void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
        => _underlying.WriteAsPropertyName(writer, value, options);
}

Upon encountering an unknown value, the default keyword yields the default enum value, i.e., Undefined for Currency.

[Test]
public void It_should_deserialize_unknown_enum_value()
{
    // Given
    var json = """
               [
                   {"Currency": "EUR", "Amount": 1},
                   {"Currency": "USD", "Amount": 2},
                   {"Currency": "Bitcoin", "Amount": 3}
               ]
               """;

    // When
    var actual = JsonSerializer.Deserialize<Money[]>(json, new JsonSerializerOptions
    {
        Converters = { new UnknownEnumConverter() }
    });

    //Then
    actual.Should().BeEquivalentTo(new[]
    {
        new Money(Currency.EUR, 1),
        new Money(Currency.USD, 2),
        new Money(default, 3),
    });
}

System.Xml

This one is a bit tricky, but still manageable. The XmlSerializer does not feature any serialization converters like typical JSON libraries do, but achieving our goal is still possible. However, this necessitates modifications to the C# model. Let’s examine how our model appeared previously.

public class Money
{
    public Money()
    {
    }

    public Currency Currency { get; set; }
    public decimal Amount { get; set; }
}

Let’s see how it will be structured to handle unknown enums.

public class Money
{
    public Money()
    {
    }

    [XmlIgnore] 
    public Currency CurrencyAsEnum { get; set; }

    public string Currency
    {
        get => CurrencyAsEnum.ToString("G");
        set => CurrencyAsEnum = Enum.TryParse<Currency>(value, out var result)
            ? result
            : default;
    }

    public decimal Amount { get; set; }
}

Essentially, the Currency property has been changed to a string, and a CurrencyAsEnum property has been introduced to hold a currency value as an enum. This adjustment facilitates the deserialization process.

[Test]
public void It_should_deserialize_unknown_enum_value()
{
    // Given
    var xml = """
              <ArrayOfMoney>
                  <Money><Currency>EUR</Currency><Amount>1</Amount></Money>
                  <Money><Currency>USD</Currency><Amount>2</Amount></Money>
                  <Money><Currency>Bitcoin</Currency><Amount>3</Amount></Money>
              </ArrayOfMoney>
              """;

    // When
    var serializer = new XmlSerializer(typeof(Money[]));
    var xmlAsStream = new MemoryStream(Encoding.UTF8.GetBytes(xml));
    var actual = serializer.Deserialize(xmlAsStream);

    // Then
    actual.Should().BeEquivalentTo(new[]
    {
        new Money { CurrencyAsEnum = Currency.EUR, Amount = 1 },
        new Money { CurrencyAsEnum = Currency.USD, Amount = 2 },
        new Money { CurrencyAsEnum = Currency.Undefined, Amount = 3 }
    });
}

With these adaptations, the application will sustain its functionality even upon the introduction of new currencies by the bank. Transactions involving unfamiliar currencies will be represented as Currency.Undefined within the application.

Ensuring the recognition of Currency.Undefined currency is pivotal to prevent disruptions in the business logic operations.

Frequently Asked Questions

Why do unknown enum values cause deserialization issues in .NET?

By default, when .NET libraries encounter an unknown enum value during deserialization, they throw exceptions as this value doesn’t match any predefined enum member. This can break the integration with APIs introducing new values.

How can handling unknown enums benefit API integration?

Handling unknown enums gracefully ensures that the introduction of new enum values in the integrated API does not break the existing functionality of your application. This promotes robustness and adaptability in your system.

Can the solutions provided be applied to all .NET serialization libraries?

The article provides solutions for Newtonsoft.Json, System.Text.Json, and System.Xml. While these are common libraries, the approach might differ for other libraries, so be sure to check the respective documentation.

What should I do with the Undefined enum value in my business logic?

It is essential to handle Undefined or default enum values in your business logic to avoid unexpected behavior. You might log them for analysis, request updates to your enum definitions, or implement fallback logic depending on the use case.

Where can I find the source code used in this blog post?

All related source code is available on Gaev.Blog.EnumAsStringTrap for further exploration and implementation.

In Conclusion

Employing enums is a robust method for representing named values within your application. However, when interacting with external APIs, it’s imperative to anticipate and gracefully handle potential changes in enum values. This article has elucidated how to modify various .NET serializers to accommodate unknown enum values, thereby ensuring the seamless and uninterrupted integration of your application with external APIs.

Hey, thanks a bunch for swinging by and reading the article! Did it spark any thoughts or questions? Maybe you’ve got your own experiences with .NET serialization and unknown enums? Don’t be a stranger—drop a comment below and let’s get chatting! Your insights could be the golden nuggets someone else is searching for. Cheers to sharing and learning together!