redis/jedis · error · IllegalArgumentException

Aggregation requires exactly one spec per key: numkeys=

Error message

Aggregation requires exactly one spec per key: numkeys=${numKeys} but got ${aggregators.length} aggregation spec(s)

What it means

TSNRangeParams.validateAggregationForKeys(int numKeys) is called by UnifiedJedis for TS.MRANGE/TS.MREVRANGE when the number of keys is known. If aggregation specs were set but their count differs from numKeys, the server contract (one AGGREGATION spec per key) would be violated, so the client throws IllegalArgumentException. It is a no-op when aggregation was never set (aggregators == null).

Solutions

  1. Align the spec array length with the number of keys: supply exactly one AggregationType[] per key before executing the command.
  2. If the same params must serve varying key counts, build a fresh TSNRangeParams per call and size the specs to keys.size().
  3. Call params.validateAggregationForKeys(keys.size()) yourself before executing to catch mismatches at the call site with a clear stack trace.

Example fix

// before
params.aggregation(new AggregationType[][]{{AggregationType.AVG}}, 60000); // 1 spec
jedis.tsNRange(keys, params); // keys.size() == 3 -> throws
// after
AggregationType[][] specs = new AggregationType[keys.size()][];
for (int i = 0; i < keys.size(); i++) specs[i] = new AggregationType[]{AggregationType.AVG};
params.aggregation(specs, 60000);
jedis.tsNRange(keys, params);
Defensive patterns

Strategy: validation

Validate before calling

if (params != null && keys.size() != expectedSpecCount) {
  throw new IllegalStateException("specs=" + expectedSpecCount + " keys=" + keys.size());
}
jedis.tsNRange(keys, params);

Try / catch

try { jedis.tsNRange(keys, params); } catch (IllegalArgumentException e) { log.error("aggregation spec count mismatch for {} keys", keys.size(), e); throw e; }

Prevention

When it happens

Trigger: Executing tsNRange/tsNRevRange with N keys while TSNRangeParams.aggregation(...) was given fewer or more than N AggregationType[] specs, e.g. 3 keys with only 2 specs.

Common situations: Key list built dynamically at runtime while the spec array was built from a different (stale or filtered) collection; copy-pasted params objects reused across commands with different key counts.

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 redis/jedis@6dac31d4c2 (2026-09-08). Data as JSON: /api/errors/dd366b551216bc86. Report an issue: GitHub.

Appendix: source

Thrown at src/main/java/redis/clients/jedis/timeseries/TSNRangeParams.java:197

    }
    this.aggregators = aggregators;
    this.bucketDuration = bucketDuration;
    return this;
  }

  /**
   * Validates this aggregation configuration against the actual number of command keys.
   * {@code TS.NRANGE}/{@code TS.NREVRANGE} require exactly one aggregation spec per key, so when
   * aggregation is set the number of specs must equal {@code numKeys}. This mirrors the server rule
   * client-side so callers fail fast instead of relying on the {@code the number of AGGREGATION
   * arguments must be equal to numkeys} error. No-op when aggregation is not set.
   * @param numKeys number of keys passed to the command
   * @throws IllegalArgumentException if aggregation is set and the spec count differs from
   *           {@code numKeys}
   */
  public void validateAggregationForKeys(int numKeys) {
    if (aggregators != null && aggregators.length != numKeys) {
      throw new IllegalArgumentException("Aggregation requires exactly one spec per key: numkeys="
          + numKeys + " but got " + aggregators.length + " aggregation spec(s)");
    }
  }

  /**
   * This requires AGGREGATION.
   */
  public TSNRangeParams bucketTimestamp(String bucketTimestamp) {
    this.bucketTimestamp = encode(bucketTimestamp);
    return this;
  }

  /**
   * This requires AGGREGATION.
   */
  public TSNRangeParams bucketTimestampLow() {
    this.bucketTimestamp = MINUS;
    return this;

View on GitHub (pinned to 6dac31d4c2)