Humanizr/Humanizer · error · InvalidOperationException

Unsupported number-to-words contract member kind '{member.Ki

Error message

Unsupported number-to-words contract member kind '{member.Kind}'.

What it means

The number-to-words contract factory maps each contract member 'kind' to a C# expression generator via a switch expression. If a member's kind is not one of the ~20 supported kinds (string, int64, enum, builder, profile-object, etc.), the default arm throws. This is a generator-level configuration error, not a locale YAML issue.

Source

Thrown at src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/NumberToWordsEngineContractFactory.cs:423

                "string-array" => CreateStringArrayExpression(EngineContractUtilities.GetRequiredElement(root, member.SourcePath)),
                "optional-string-array" => EngineContractUtilities.TryGetElement(root, member.SourcePath, out var optionalArray)
                    ? CreateStringArrayExpression(optionalArray)
                    : "Array.Empty<string>()",
                "int-string-dictionary" => CreateStringIntFrozenDictionaryExpression(EngineContractUtilities.GetRequiredElement(root, member.SourcePath)),
                "nullable-int-string-dictionary" => CreateNullableIntStringDictionaryValue(root, member),
                "string-string-dictionary" => CreateStringStringFrozenDictionaryExpression(EngineContractUtilities.GetRequiredElement(root, member.SourcePath)),
                "nullable-string-string-dictionary" => EngineContractUtilities.TryGetElement(root, member.SourcePath, out var optionalStringDictionary)
                    ? CreateOptionalStringStringFrozenDictionaryExpression(root, EngineContractUtilities.GetLeafPropertyName(member.SourcePath))
                    : member.MissingValue == "empty"
                        ? "FrozenDictionary<string, string>.Empty"
                        : "null",
                "char-string-dictionary" => CreateCharStringFrozenDictionaryExpression(EngineContractUtilities.GetRequiredElement(root, member.SourcePath)),
                "nullable-char-string-dictionary" => EngineContractUtilities.TryGetElement(root, member.SourcePath, out var optionalCharDictionary)
                    ? CreateCharStringFrozenDictionaryExpression(optionalCharDictionary)
                    : "null",
                "nullable-int-set" => CreateNullableIntSetValue(root, member),
                "builder" => CreateBuilderValue(root, member),
                _ => throw new InvalidOperationException($"Unsupported number-to-words contract member kind '{member.Kind}'.")
            };

        static string CreateObjectValue(JsonElement root, EngineContractMember member, bool useCultureParameter)
        {
            var objectRoot = string.IsNullOrWhiteSpace(member.SourcePath)
                ? root
                : EngineContractUtilities.GetRequiredElement(root, member.SourcePath);

            // Nested profile objects let the YAML stay grouped by meaning while the generated code
            // still calls an explicit runtime constructor shape. This keeps the authoring surface
            // readable without paying any runtime mapping cost.
            return member.TypeName is null
                ? "new(" + CreateConstructorValues(objectRoot, member.Members, useCultureParameter) + ")"
                : "new " + member.TypeName + "(" + CreateConstructorValues(objectRoot, member.Members, useCultureParameter) + ")";
        }

        static string CreateEnumValue(JsonElement root, EngineContractMember member)
        {

View on GitHub (pinned to ffc2b77c0f)

Solutions

  1. Add a matching case to the CreateMemberValue switch expression in NumberToWordsEngineContractFactory.cs for the new kind.
  2. If the kind was mistyped, correct the Kind value in the EngineContractMember definition to match a supported kind.
  3. Review the EngineContractCatalog definitions for the affected engine and verify every member kind has a factory case.

Example fix

// before (contract member with unsupported kind)
new EngineContractMember(
    kind: "decimal",
    sourcePath: "factor",
    ...)
// after — use a supported kind or add a factory case
new EngineContractMember(
    kind: "int64",
    sourcePath: "factor",
    ...)
Defensive patterns

Strategy: validation

Validate before calling

// Validate all contract member kinds are supported before generation:
static readonly HashSet<string> SupportedKinds = new()
{
    "profile-object","optional-profile-object","culture","string","nullable-string",
    "bool","presence-bool","int64","nullable-int64","int32","enum",
    "string-array","optional-string-array","int-string-dictionary",
    "nullable-int-string-dictionary","string-string-dictionary",
    "nullable-string-string-dictionary","char-string-dictionary",
    "nullable-char-string-dictionary","nullable-int-set","builder"
};
foreach (var m in contract.Members)
    if (!SupportedKinds.Contains(m.Kind))
        throw new ArgumentException($"Unsupported kind '{m.Kind}' in contract.");

Type guard

static bool IsSupportedMemberKind(string kind) =>
    SupportedKinds.Contains(kind);

Prevention

When it happens

Trigger: An EngineContractMember is defined with a Kind value not handled by the switch in CreateMemberValue. For example, a new kind 'decimal' was added to a contract definition but no corresponding case exists in the factory. Typically introduced by editing EngineContractCatalog.

Common situations: Adding a new member kind to a contract definition without adding a factory case. Renaming a kind in one place but not the other. Version mismatch between contract definitions and the factory after a partial merge.

Related errors


AI-assisted analysis of Humanizr/Humanizer@ffc2b77c0f (2026-08-13). Data as JSON: /api/errors/958d609c53940ec3. Report an issue: GitHub.