languagetool-org/languagetool · error · RuntimeException

Duplicate key '${key}'

Error message

Duplicate key '${key}'

What it means

RuleFilterEvaluator.getResolvedArguments throws this when two filter arguments resolve to the same key name, meaning the rule declared duplicate argument names for a filter. The result map is built key-by-key from the args attribute, and duplicates would silently overwrite each other, so the evaluator fails fast. It usually reflects a typo or copy-paste in the 'args' attribute of a <filter> element.

Source

Thrown at languagetool-core/src/main/java/org/languagetool/rules/patterns/RuleFilterEvaluator.java:73

    for (String arg : arguments) {
      int delimPos = arg.indexOf(':');
      if (delimPos == -1) {
        throw new RuntimeException("Invalid syntax for key/value, expected 'key:value', got: '" + arg + "'");
      }
      String key = arg.substring(0, delimPos);
      String val = arg.substring(delimPos + 1);
      if (val.startsWith("\\")) {
        int refNumber = Integer.parseInt(val.replace("\\", ""));
        if (refNumber > tokenPositions.size()) {
          throw new RuntimeException("Your reference number " + refNumber + " is bigger than the number of tokens: " + tokenPositions.size());
        }
        int correctedRef = getSkipCorrectedReference(tokenPositions, refNumber);
        if (correctedRef >= patternTokens.length) {
          throw new RuntimeException("Your reference number " + refNumber +
                  " is bigger than number of matching tokens: " + patternTokens.length);
        }
        if (result.containsKey(key)) {
          throw new RuntimeException("Duplicate key '" + key + "'");
        }
        result.put(key, patternTokens[correctedRef].getToken());
      } else {
        result.put(key, val);
      }
    }
    return result;
  }

  // when there's a 'skip', we need to adapt the reference number
  private int getSkipCorrectedReference(List<Integer> tokenPositions, int refNumber) {
    int correctedRef = 0;
    int i = 0;
    for (int tokenPosition : tokenPositions) {
      if (i++ >= refNumber) {
        break;
      }
      correctedRef += tokenPosition;

View on GitHub (pinned to 2e990059ce)

Solutions

  1. Edit the filter's args attribute so every key is unique
  2. Rename the duplicate argument and update the filter Java class to use the new parameter name
  3. Validate rule XML in CI (LanguageTool's rule validation / tests) to catch duplicate keys early

Example fix

// before
<filter class="..." args="no:1 no:2"/>
// after
<filter class="..." args="no:1 postag:2"/>
Defensive patterns

Strategy: validation

Validate before calling

Set<String> seen = new HashSet<>();
for (String pair : argsString.split(",")) {
  String key = pair.split(":")[0].trim().toLowerCase(Locale.ROOT);
  if (!seen.add(key)) throw new IllegalArgumentException("Duplicate filter arg key: " + key);
}

Try / catch

try {
  result = evaluator.getResolvedArguments(filterId, args, matches, patternTokens);
} catch (RuntimeException e) {
  if (e.getMessage().startsWith("Duplicate key")) {
    log.error("Fix duplicate args key in rule XML", e);
  } else throw e;
}

Prevention

When it happens

Trigger: A <filter args='word:1 word:2'/> or args string containing the same key twice (e.g. 'no:1 No:2' after normalization) is evaluated; getResolvedArguments checks result.containsKey(key) before each put.

Common situations: Copy-pasting filter argument lists, renaming one argument but leaving the old one in place, generating args strings programmatically with repeated keys, case-insensitive key collisions.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of languagetool-org/languagetool@2e990059ce (2026-09-06). Data as JSON: /api/errors/bebec47bd0dc36ac. Report an issue: GitHub.