languagetool-org/languagetool · error · BadRequestException

'data' key in JSON requires either 'text' or 'annotation' ke

Error message

'data' key in JSON requires either 'text' or 'annotation' key, not both

What it means

The 'data' JSON payload of /v2/check may contain either a 'text' key (to be annotated per 'annotatedText' rules) or an 'annotation' key (explicit token list), but not both, since the two modes conflict. Supplying both keys throws BadRequestException (HTTP 400).

Source

Thrown at languagetool-server/src/main/java/org/languagetool/server/ApiV2.java:160

    ServerMetricsCollector.getInstance().logResponse(HttpURLConnection.HTTP_OK);
  }

  private void handleCheckRequest(HttpExchange httpExchange, Map<String, String> parameters, ErrorRequestLimiter errorRequestLimiter, String remoteAddress, HTTPServerConfig config) throws Exception {
    AnnotatedText aText;
    if (parameters.containsKey("text") && parameters.containsKey("data")) {
      throw new BadRequestException("Set only 'text' or 'data' parameter, not both");
    } else if (parameters.containsKey("text")) {
      aText = new AnnotatedTextBuilder().addText(parameters.get("text")).build();
    } else if (parameters.containsKey("data")) {
      ObjectMapper mapper = new ObjectMapper();
      JsonNode data;
      try {
        data = mapper.readTree(parameters.get("data"));
      } catch (JsonProcessingException e) {
        throw new BadRequestException("Could not parse JSON from 'data' parameter", e);
      }
      if (data.get("text") != null && data.get("annotation") != null) {
        throw new BadRequestException("'data' key in JSON requires either 'text' or 'annotation' key, not both");
      } else if (data.get("text") != null) {
        aText = getAnnotatedTextFromString(data, data.get("text").asText());
      } else if (data.get("annotation") != null) {
        aText = getAnnotatedTextFromJson(data);
      } else {
        throw new BadRequestException("'data' key in JSON requires 'text' or 'annotation' key");
      }
    } else {
      throw new BadRequestException("Missing 'text' or 'data' parameter");
    }
    //get from config
    if (config.logIp && aText.getPlainText().trim().equals(config.logIpMatchingPattern)) {
      handleIpLogMatch(httpExchange, remoteAddress, parameters);
      //no need to check text again rules
      return;
    }
    textChecker.checkText(aText, httpExchange, parameters, errorRequestLimiter, remoteAddress);
  }

View on GitHub (pinned to 2e990059ce)

Solutions

  1. Remove one of the two keys — keep 'text' for builder-style input or 'annotation' for explicit tokens
  2. If both pieces of info are needed, convert the text into the annotation format
  3. Add a client-side check that at most one key is set before sending

Example fix

// before
{"text":"Hello world","annotation":[{"text":"Hello"},{"text":"world"}]}
// after
{"annotation":[{"text":"Hello"},{"text":"world"}]}
Defensive patterns

Strategy: validation

Validate before calling

const d = JSON.parse(dataPayload);
if (d.text != null && d.annotation != null) {
    throw new Error("'text' and 'annotation' are mutually exclusive");
}

Try / catch

try {
    const res = await check({ data: JSON.stringify(d) });
} catch (e) {
    if (/not both/.test(e.message)) {
        console.error('Keep only text OR annotation in data JSON');
    } else throw e;
}

Prevention

When it happens

Trigger: POST /v2/check with data={"text":"...","annotation":[...]} — both keys present in the parsed JSON object.

Common situations: Migrating clients that append an annotation array while keeping the legacy text field; template code that fills both keys conditionally.

Related errors


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