{"record":{"id":"e9007853c11ea89c","repo":"Humanizr/Humanizer","slug":"custom-formatter-type-gettype-fullname-must","errorCode":null,"errorMessage":"Custom formatter type '{GetType().FullName}' must explicitly implement IGrammaticalCaseTimeSpanFormatter to support grammatical-case-aware durations.","messagePattern":"Custom formatter type '(.+?)' must explicitly implement IGrammaticalCaseTimeSpanFormatter to support grammatical-case-aware durations\\.","errorType":"exception","errorClass":"NotSupportedException","httpStatus":null,"severity":"error","filePath":"src/Humanizer/Localisation/Formatters/DefaultFormatter.cs","lineNumber":138,"sourceCode":"\n        if (!toSymbols &&\n            (!phraseTable.TryGetTimeSpanPhrase(TimeUnit.Second, out var phrase) ||\n             phrase.Multiple is null))\n        {\n            throw new InvalidOperationException($\"Missing generated fractional-second phrase for '{Culture.Name}'.\");\n        }\n\n        return category;\n    }\n\n    string IGrammaticalCaseTimeSpanFormatter.TimeSpanHumanize(\n        TimeUnit timeUnit,\n        int unit,\n        GrammaticalCase grammaticalCase)\n    {\n        if (GetType().Assembly != typeof(DefaultFormatter).Assembly)\n        {\n            throw new NotSupportedException(\n                $\"Custom formatter type '{GetType().FullName}' must explicitly implement {nameof(IGrammaticalCaseTimeSpanFormatter)} to support grammatical-case-aware durations.\");\n        }\n\n        if ((uint)grammaticalCase > (uint)GrammaticalCase.Causal)\n        {\n            throw new ArgumentOutOfRangeException(nameof(grammaticalCase), grammaticalCase, \"Unsupported grammatical case.\");\n        }\n\n        if ((uint)timeUnit > (uint)TimeUnit.Year)\n        {\n            throw new ArgumentOutOfRangeException(nameof(timeUnit), timeUnit, \"Unsupported time unit.\");\n        }\n\n        var table = LocaleDurationCaseTableCatalog.Resolve(Culture)\n            ?? throw new NotSupportedException(\n                $\"Culture '{Culture.Name}' has no grammatical-case classification for duration phrases.\");\n\n        if (table.Classification == LocaleDurationCaseClassification.Unsupported)","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/Humanizr/Humanizer/blob/ffc2b77c0f30d2fb176875841424379319d0ae9b/src/Humanizer/Localisation/Formatters/DefaultFormatter.cs#L120-L156","documentation":"Thrown by the explicit IGrammaticalCaseTimeSpanFormatter.TimeSpanHumanize implementation on DefaultFormatter when the formatter instance is a subclass defined outside the Humanizer assembly. The base implementation relies on internal generated case tables that only the built-in formatters can resolve, so any custom subclass that does not override this interface member is rejected. The fix is for the subclass to explicitly implement IGrammaticalCaseTimeSpanFormatter itself.","triggerScenarios":"Subclassing DefaultFormatter (e.g. public class MyFormatter : DefaultFormatter) in your own assembly and then invoking a case-aware duration path (one that calls IGrammaticalCaseTimeSpanFormatter.TimeSpanHumanize with a GrammaticalCase). The GetType().Assembly != typeof(DefaultFormatter).Assembly check detects the external subclass and throws NotSupportedException.","commonSituations":"Custom locale formatters that extend DefaultFormatter to tweak a few phrases; plugging a specialized formatter into Humanizer's registry for an unsupported dialect; upgrading Humanizer to a version that introduced case-aware formatting and discovering your subclass no longer works for Slavic/Baltic-style languages.","solutions":["Have your custom formatter explicitly implement IGrammaticalCaseTimeSpanFormatter.TimeSpanHumanize(TimeUnit, int, GrammaticalCase) to provide case-aware output for your locale.","If your locale has no grammatical-case system, implement the interface to return the nominative form (delegating to the base TimeSpanHumanize(TimeUnit, int) overload).","Avoid subclassing DefaultFormatter for case-aware locales; instead contribute the locale data so the generated tables include your culture."],"exampleFix":"// before\npublic class MyFormatter : DefaultFormatter\n{\n    public MyFormatter(CultureInfo c) : base(c) { }\n    // no IGrammaticalCaseTimeSpanFormatter override -> throws on case-aware calls\n}\n\n// after\npublic class MyFormatter : DefaultFormatter, IGrammaticalCaseTimeSpanFormatter\n{\n    public MyFormatter(CultureInfo c) : base(c) { }\n\n    string IGrammaticalCaseTimeSpanFormatter.TimeSpanHumanize(\n        TimeUnit timeUnit, int unit, GrammaticalCase grammaticalCase) =>\n        TimeSpanHumanize(timeUnit, unit); // fall back to nominative\n}","handlingStrategy":"type-guard","validationCode":"// Before relying on the case-aware path with a custom formatter subclass,\n// ensure it explicitly implements the interface:\nif (formatter is not IGrammaticalCaseTimeSpanFormatter)\n    throw new InvalidOperationException(\"Formatter cannot handle grammatical case.\");","typeGuard":"static bool SupportsCaseAwareDurations(IFormatter f) =>\n    f.GetType().Assembly == typeof(DefaultFormatter).Assembly ||\n    f is IGrammaticalCaseTimeSpanFormatter;","tryCatchPattern":"try { return ((IGrammaticalCaseTimeSpanFormatter)formatter).TimeSpanHumanize(unit, count, case); }\ncatch (NotSupportedException) { return formatter.TimeSpanHumanize(unit, count); }","preventionTips":["If you subclass DefaultFormatter, explicitly implement IGrammaticalCaseTimeSpanFormatter.","Prefer composition or contributing locale data over subclassing for case-aware locales.","Test custom formatters against case-aware duration calls, not just nominative ones."],"tags":["formatter","localization","grammatical-case","inheritance","not-supported"],"backgroundTag":null,"analyzedSha":"ffc2b77c0f30d2fb176875841424379319d0ae9b","analyzedAt":"2026-08-13T21:42:34.584Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}