languagetool-org/languagetool · error · BadRequestException

Only either 'text' or 'markup' are supported in an object in

Error message

Only either 'text' or 'markup' are supported in an object in 'annotation' list, not both: <node>

What it means

When building AnnotatedText from the JSON 'annotation' list, each object may specify 'text' or 'markup', not both. LanguageTool throws BadRequestException when a single annotation object contains both keys, because it cannot decide whether the content is checkable text or non-checked markup.

Source

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

        }
      }
    }
    return textBuilder.build();
  }

  private AnnotatedText getAnnotatedTextFromJson(JsonNode data) {
    AnnotatedTextBuilder atb = new AnnotatedTextBuilder();
    // Expected format:
    // annotation: [
    //   {text: 'text'},
    //   {markup: '<b>'}
    //   {text: 'more text'},
    //   {markup: '</b>'}
    // ]
    //
    for (JsonNode node : data.get("annotation")) {
      if (node.get("text") != null && node.get("markup") != null) {
        throw new BadRequestException("Only either 'text' or 'markup' are supported in an object in 'annotation' list, not both: " + node);
      } else if (node.get("text") != null && node.get("interpretAs") != null) {
        throw new BadRequestException("'text' cannot be used with 'interpretAs' (only 'markup' can): " + node);
      } else if (node.get("text") != null) {
        atb.addText(node.get("text").asText());
      } else if (node.get("markup") != null) {
        if (node.get("interpretAs") != null) {
          atb.addMarkup(node.get("markup").asText(), node.get("interpretAs").asText());
        } else {
          atb.addMarkup(node.get("markup").asText());
        }
      } else {
        throw new BadRequestException("Only 'text' and 'markup' are supported in 'annotation' list: " + node);
      }
    }
    return atb.build();
  }

  String getLanguages() throws IOException {

View on GitHub (pinned to 2e990059ce)

Solutions

  1. Split the object into two entries: one {"markup":...} followed by one {"text":...}.
  2. Remove the 'markup' key if the content should be checked as text.
  3. Remove the 'text' key if the content is markup; use 'interpretAs' with markup if it should be checked as text.

Example fix

// before
{"annotation":[{"text":"Hello","markup":"<b>"}]}
// after
{"annotation":[{"markup":"<b>"},{"text":"Hello"}]}
Defensive patterns

Strategy: validation

Validate before calling

for (const n of annotation) {
  if ('text' in n && 'markup' in n) throw new Error('Annotation object must not have both text and markup: ' + JSON.stringify(n));
}

Type guard

function isTextOnly(n) { return n.text != null && n.markup == null && n.interpretAs == null; }
function isMarkupOnly(n) { return n.markup != null && n.text == null; }

Try / catch

try {
  const res = await fetch('/v2/check', { method: 'POST', body: form });
  if (!res.ok) throw new Error((await res.json()).message);
} catch (e) {
  console.error('annotation payload invalid:', e.message);
}

Prevention

When it happens

Trigger: POSTing to /v2/check with data JSON like {"annotation":[{"text":"Hello","markup":"<b>"}]} — any annotation object having both a non-null 'text' and a non-null 'markup' key.

Common situations: Clients programmatically generating the annotation list that accidentally include markup alongside text; hand-written payloads where markup was pasted into a text object.

Related errors


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