Skip to content

Replace standalone C# enums with forward-compatible IEnum class pattern#1680

Merged
gcatanese merged 4 commits into
mainfrom
1650-standalone-enums-forward-compatibility
May 21, 2026
Merged

Replace standalone C# enums with forward-compatible IEnum class pattern#1680
gcatanese merged 4 commits into
mainfrom
1650-standalone-enums-forward-compatibility

Conversation

@gcatanese
Copy link
Copy Markdown
Contributor

@gcatanese gcatanese commented May 12, 2026

Description

Fix #1650 #1612

Problem

There are 2 types of enum values in the .NET library:

  • inner enums: they define an IEnum class that it is forward-compatible, new (unknown) values are handled
  • standalone enums: they define standalone public enums (with int backing values), new (unknown) values throw an exception

Solution

Replaces all standalone C# enum types generated by modelEnum.mustache with an IEnum class pattern, matching the existing inner-enum approach. Unknown API values are now preserved as-is instead of throwing JsonException or causing NullReferenceException (fixes #1612).

The PR updates the Mustache templates and generates Checkout models (to create additional tests for the different type of enums) and BalancePlatform models (to fix a failing test).

Important

👍 Handling of standalone enum values has been improved: new enum values do not thrown an error, they instead retain the string value found in the JSON payload. This was the goal of the PR.

⚠️ However, a similar change has been applied to inline enum values: unknown enum values are no longer deserialized as null, but they instead retain the string value found in the JSON payload.
This is an improvement but also a breaking change: existing code that relies on null checks to identify unknown enum values will no longer work.

Changes

  • modelEnum.mustache: override default template and generate IEnum classes for standalone enums with FromStringOrDefault, ToJsonValue, implicit string conversions, and a nested JsonConverter
  • modelInnerEnum.mustache: ToJsonValue returns raw string for unknown values instead of null
  • JsonConverter.mustache: read path preserves unknown enum values; required enum assignment removes .Value unwrap; write path uses T.ToJsonValue(); oneOf discriminator resolution guards against unknown type values via FromStringOrDefault
  • HostConfiguration.mustache: converter registration updated to nested class form; NullableJsonConverter removed
  • modelGeneric.mustache: discriminator init uses implicit string cast instead of Enum.Parse
  • ClientUtils.mustache: ToJsonValue call updated for standalone enums

Tested scenarios

Affected Standalone Enums (18 files)

18 standalone enums listed above change from enum (value type) to class : IEnum (reference type).

Module Enums
BalancePlatform LimitStatus, AssociationStatus, Scope, ScaDeviceType, ScaExemption, MandateStatus, MandateType, SettingType, ScaStatus, ScaEntityType, TransferType
Capital CALocalBankAccountType, AdditionalBankIdentificationTypes, FundsCollectionType, USLocalBankAccountType
Checkout Result
SessionAuthentication ResourceType, ProductType

BEFORE (current SDK):

// Type is a C# enum (value type)
LimitStatus status = LimitStatus.Active;

// Switch statement
switch (status)
{
    case LimitStatus.Active:
        Console.WriteLine("Active");
        break;
    case LimitStatus.Inactive:
        Console.WriteLine("Inactive");
        break;
}

// Nullable handling (Nullable<T> value type)
LimitStatus? optional = GetStatus();
if (optional.HasValue)
{
    LimitStatus unwrapped = optional.Value;
}

// Default value
LimitStatus def = default; // = 0 (undefined int-backed value)

// Cast to int
int numeric = (int)status; // = 1

// Crashes on unknown API values:
// JsonException or NullReferenceException (.NET 8)

AFTER (new SDK):

// Type is now an IEnum class (reference type)
LimitStatus status = LimitStatus.Active;

// Switch statement -> if/else or pattern matching
if (status == LimitStatus.Active)
    Console.WriteLine("Active");
else if (status == LimitStatus.Inactive)
    Console.WriteLine("Inactive");

// Nullable handling (standard null check)
LimitStatus? optional = GetStatus();
if (optional != null)
{
    LimitStatus unwrapped = optional; // no .Value needed
}

// Default value
LimitStatus def = default; // = null (reference type)

// Cast to int no longer works
// int numeric = (int)status; // COMPILE ERROR

// String value access
string raw = status.Value; // "active"

// Unknown API values are preserved (no crash):
LimitStatus unknown = (LimitStatus)"future-status";
Console.WriteLine(unknown.Value); // "future-status"
Console.WriteLine(unknown == LimitStatus.Active); // false

Summary of breaking changes

Change Impact Migration
enum -> class switch/case no longer works Use if/== or pattern matching
Value type -> Reference type .HasValue/.Value on nullable removed Use != null instead of .HasValue
default changes from 0 to null Variables default to null Initialize explicitly
(int) cast removed Numeric conversion fails Use .Value string property
Enum.Parse/Enum.GetValues removed Reflection-based enum APIs fail Use FromStringOrDefault / static fields

@gcatanese gcatanese requested a review from a team as a code owner May 12, 2026 11:05
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@gcatanese gcatanese force-pushed the 1650-standalone-enums-forward-compatibility branch from 572eec7 to 115781c Compare May 12, 2026 11:14
@gcatanese
Copy link
Copy Markdown
Contributor Author

@gemini-code-assist review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a new IEnum pattern for generated C# models to improve forward compatibility and round-trip safety by preserving unknown enum values as raw strings. The changes involve significant updates to mustache templates, JSON converters, and numerous generated model files, along with the addition of comprehensive tests. A review comment identifies an improvement opportunity to make the Value property on the generated IEnum classes readonly, ensuring immutability and preventing potential bugs caused by consumers modifying enum instances.

Comment thread templates-v7/csharp/libraries/generichost/modelEnum.mustache
Comment thread Adyen.Test/Checkout/IEnumCheckoutTest.cs
Copy link
Copy Markdown
Contributor

@galesky-a galesky-a left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, best to tackle those asap so we have the same pattern for all enums moving forward. Left a single comment on null behavior but it's just a sanity check

@gcatanese gcatanese added this pull request to the merge queue May 21, 2026
Merged via the queue into main with commit 7115df7 May 21, 2026
2 checks passed
@gcatanese gcatanese deleted the 1650-standalone-enums-forward-compatibility branch May 21, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants