apache/druid · error · IllegalStateException

Conflicting key[ ] calculated via keyMapper for original…

Error message

Conflicting key[%s] calculated via keyMapper for original key[%s]

What it means

CollectionUtils.mapKeys builds a new map by applying keyMapper to each key; since a plain HashMap allows only one entry per resulting key, a mapper that maps two different original keys to the same new key throws this IllegalStateException. It protects callers from silently losing values during key transformation.

Solutions

  1. Make the keyMapper injective over the input keys — include any distinguishing component that was dropped (case, prefix, suffix)
  2. Use mapValues or a different collector (e.g. groupingBy with a merge function) if collisions are expected and values should be combined
  3. Deduplicate the input map's keys first if collisions are known and one value should win
  4. Pre-check the mapper by collecting mapped keys into a Set and comparing sizes before calling mapKeys

Example fix

// before
Map<String, V> out = CollectionUtils.mapKeys(map, k -> k.toLowerCase()); // collides on case
// after
Map<String, V> out = CollectionUtils.mapKeys(map, k -> k); // keep original keys, or merge explicitly
Map<String, V> merged = map.entrySet().stream().collect(
    Collectors.toMap(e -> e.getKey().toLowerCase(), Map.Entry::getValue, (a, b) -> a));
Defensive patterns

Strategy: validation

Validate before calling

Set<Object> mapped = map.keySet().stream().map(keyMapper).collect(Collectors.toSet());
if (mapped.size() != map.size()) {
  throw new IllegalStateException("keyMapper is not injective over input keys");
}

Try / catch

try {
  result = CollectionUtils.mapKeys(map, keyMapper);
} catch (IllegalStateException e) {
  if (e.getMessage() != null && e.getMessage().contains("Conflicting key")) {
    // rebuild with a merging collector
  } else throw e;
}

Prevention

When it happens

Trigger: Calling CollectionUtils.mapKeys with a Function whose mapping is not injective over the input map's keys — e.g. mapping keys to lowercase strings, or dropping distinguishing prefixes/suffixes — when the source map contains two keys that collide.

Common situations: Normalizing segment/data-source names (case-folding) before mapping; stripping version or tenant prefixes from keys; mapping enum keys to strings where two enum values share a name.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of apache/druid@9b90983fd2 (2026-09-07). Data as JSON: /api/errors/96e2768262dc755a. Report an issue: GitHub.

Appendix: source

Thrown at processing/src/main/java/org/apache/druid/utils/CollectionUtils.java:122

    final Map<K, V2> result = Maps.newHashMapWithExpectedSize(map.size());
    map.forEach((k, v) -> result.put(k, valueMapper.apply(v)));
    return result;
  }

  /**
   * Returns a transformed map from the given input map where the key is modified based on the given keyMapper
   * function. This method fails if keys collide after applying the  given keyMapper function and
   * throws a IllegalStateException.
   *
   * @throws ISE if key collisions occur while applying specified keyMapper
   */
  public static <K, V, K2> Map<K2, V> mapKeys(Map<K, V> map, Function<K, K2> keyMapper)
  {
    final Map<K2, V> result = Maps.newHashMapWithExpectedSize(map.size());
    map.forEach((k, v) -> {
      final K2 k2 = keyMapper.apply(k);
      if (result.putIfAbsent(k2, v) != null) {
        throw new ISE("Conflicting key[%s] calculated via keyMapper for original key[%s]", k2, k);
      }
    });
    return result;
  }

  /**
   * Creates an immutable map by mapping each entry in the given collection to
   * a key and a value.
   */
  public static <E, K, V> Map<K, V> toMap(Collection<E> collection, Function<E, K> keyMapper, Function<E, V> valueMapper)
  {
    return collection.stream().collect(Collectors.toMap(keyMapper, valueMapper));
  }

  /**
   * Returns a LinkedHashMap with an appropriate size based on the callers expectedSize. This methods functionality
   * mirrors that of com.google.common.collect.Maps#newLinkedHashMapWithExpectedSize in Guava 19+. Thus, this method
   * can be replaced with Guava's implementation once Druid has upgraded its Guava dependency to a sufficient version.

View on GitHub (pinned to 9b90983fd2)