Humanizr/Humanizer · error · InvalidOperationException

'{path}' must define all duration 'units'.

Error message

'{path}' must define all duration 'units'.

What it means

Thrown by ParseCase when a case overlay entry (under durationCases.cases.<caseName>) does not contain a 'units' key. Every case overlay must define all eight duration units explicitly, so the parser rejects an entry that omits the units mapping entirely.

Source

Thrown at src/Humanizer.SourceGenerators/Common/DurationCaseModels.cs:1127

            }

            return new DurationCaseOverlay(units.ToImmutable());
        }

        static ImmutableDictionary<string, DurationCaseUnitRealization> EmptyUnitRealizations() =>
            ImmutableDictionary<string, DurationCaseUnitRealization>.Empty.WithComparers(StringComparer.Ordinal);

        static ImmutableDictionary<string, DurationCaseSource> EmptySources() =>
            ImmutableDictionary<string, DurationCaseSource>.Empty.WithComparers(StringComparer.Ordinal);

        static DurationCaseOverlay ParseCase(SimpleYamlValue value, string path)
        {
            var mapping = ExpectMapping(value, path);
            RejectUnknownKeys(mapping, path, ["units"]);

            if (!mapping.TryGetValue("units", out var unitsValue))
            {
                throw new InvalidOperationException($"'{path}' must define all duration 'units'.");
            }

            var unitsMapping = ExpectMapping(unitsValue, $"{path}.units");
            RejectUnknownKeys(unitsMapping, $"{path}.units", TimeUnits);
            var units = ImmutableDictionary.CreateBuilder<string, DurationCaseUnit>(StringComparer.Ordinal);
            foreach (var unitName in TimeUnits)
            {
                if (!unitsMapping.TryGetValue(unitName, out var unitValue))
                {
                    throw new InvalidOperationException(
                        $"'{path}.units' must explicitly define '{unitName}'.");
                }

                units[unitName] = ParseUnit(unitValue, $"{path}.units.{unitName}");
            }

            return new DurationCaseOverlay(units.ToImmutable());
        }

View on GitHub (pinned to ffc2b77c0f)

Solutions

  1. Add a 'units' mapping to the case overlay entry at the reported path.
  2. Populate it with all eight time units (millisecond, second, minute, hour, day, week, month, year), each specifying a phrase, sameAsNominative, or unsupported value.
  3. Rebuild to confirm the case overlay is accepted.

Example fix

# before (case overlay missing 'units')
accusative:
  citation: Acc
# after
accusative:
  units:
    millisecond:
      sameAsNominative: true
    second:
      sameAsNominative: true
    minute:
      sameAsNominative: true
    hour:
      sameAsNominative: true
    day:
      sameAsNominative: true
    week:
      sameAsNominative: true
    month:
      sameAsNominative: true
    year:
      sameAsNominative: true
Defensive patterns

Strategy: validation

Validate before calling

# Verify every case overlay in durationCases.cases has a 'units' key
# python: for case_name, case in yaml['durationCases']['cases'].items():
#     assert 'units' in case, f'case {case_name} missing units'

Prevention

When it happens

Trigger: A case overlay entry in the 'cases' section of durationCases lacks the 'units' key, so TryGetValue('units', ...) returns false.

Common situations: A contributor adds a new case to the cases section but only provides metadata or a partial structure without the units mapping. Also occurs when a contributor confuses the case-overlay format (which needs 'units') with the realization format (which uses discriminators like phrase/sameRenderedAs).

Related errors


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