stanfordnlp/CoreNLP · error · RuntimeException

TIMEX3 should only contain text

Error message

TIMEX3 should only contain text ${child}

What it means

HeidelTimeAnnotator's toTimexCoreMaps parses HeidelTime's XML output; each TIMEX3 element produced by HeidelTime is expected to wrap exactly one text node. When a TIMEX3 element has zero or more than one child nodes (e.g. nested elements or split text), the annotator throws this RuntimeException because the offset/token mapping logic cannot handle that shape.

Solutions

  1. Verify the external heideltime.jar version matches what this CoreNLP version expects; replace with the bundled/supported version
  2. Inspect the HeidelTime XML output (enable debug printing) to find the offending TIMEX3 element and check why it has multiple children
  3. Pre-normalize the input text so HeidelTime does not emit nested tags (e.g. strip XML/HTML from the document before annotation)
  4. Patch toTimexCoreMaps to flatten multi-child TIMEX3 elements by iterating child text nodes instead of requiring exactly one

Example fix

// before: hard failure on non-flat TIMEX3
if (child.getChildNodes().getLength() != 1) {
  throw new RuntimeException("TIMEX3 should only contain text " + child);
}
// after: flatten multiple text children
StringBuilder sb = new StringBuilder();
NodeList kids = child.getChildNodes();
for (int j = 0; j < kids.getLength(); j++) {
  if (kids.item(j) instanceof Text) sb.append(kids.item(j).getTextContent());
}
String timexText = sb.toString();
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure input to HeidelTimeAnnotator is plain, tokenizable text
String docText = annotation.get(CoreAnnotations.TextAnnotation.class);
if (docText == null || docText.trim().isEmpty()) {
    throw new IllegalArgumentException("document text is empty");
}
if (docText.contains("<TIMEX3")) {
    throw new IllegalArgumentException("input already contains TIMEX3 markup; pass plain text");
}

Type guard

static boolean isFlatTimex3(Element el) {
    return "TIMEX3".equals(el.getNodeName()) && el.getChildNodes().getLength() == 1
        && el.getChildNodes().item(0) instanceof Text;
}

Try / catch

try {
    pipeline.annotate(annotation);
} catch (RuntimeException e) {
    if (e.getMessage() != null && e.getMessage().startsWith("TIMEX3 should only contain text")) {
        // fall back: skip time annotation or re-run with sanitized input
        logger.warn("HeidelTime produced non-flat TIMEX3; skipping doc " + annotation.get(CoreAnnotations.DocIDAnnotation.class));
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: Running the HeidelTimeAnnotator (or a StanfordCoreNLP pipeline containing 'heideltime') on a document whose HeidelTime output contains a TIMEX3 element with nested/multiple children — typically when HeidelTime tags a span that includes markup or the external heideltime.jar version emits non-flat TIMEX3 elements.

Common situations: Using a mismatched or customized heideltime.jar whose output format differs from what this annotator expects; documents with unusual date expressions causing HeidelTime to emit nested TIMEX3 tags; passing pre-annotated XML rather than plain text.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of stanfordnlp/CoreNLP@1b7edd19c4 (2026-09-10). Data as JSON: /api/errors/67a5c5696ac742a3. Report an issue: GitHub.

Appendix: source

Thrown at src/edu/stanford/nlp/time/HeidelTimeAnnotator.java:218

        int charEnd = token.get(CoreAnnotations.CharacterOffsetEndAnnotation.class);
        beginMap.put(charBegin,tokBegin);
        endMap.put(charEnd,tokEnd);
      }
    }
    List<CoreMap> timexMaps = new ArrayList<>();
    int offset = 0;
    NodeList docNodes = docElem.getChildNodes();
    for (int i = 0; i < docNodes.getLength(); i++) {
      Node content = docNodes.item(i);
      if (content instanceof Text) {
        Text text = (Text)content;
        offset += text.getWholeText().length();
      } else if (content instanceof Element) {
        Element child = (Element)content;
        if (child.getNodeName().equals("TIMEX3")) {
          Timex timex = new Timex(child);
          if (child.getChildNodes().getLength() != 1) {
            throw new RuntimeException("TIMEX3 should only contain text " + child);
          }
          String timexText = child.getTextContent();
          CoreMap timexMap = new ArrayCoreMap();
          timexMap.set(TimeAnnotations.TimexAnnotation.class, timex);
          timexMap.set(CoreAnnotations.TextAnnotation.class, timexText);
          int charBegin = offset;
          timexMap.set(CoreAnnotations.CharacterOffsetBeginAnnotation.class, offset);
          offset += timexText.length();
          timexMap.set(CoreAnnotations.CharacterOffsetEndAnnotation.class, offset);
          int charEnd = offset;
          //(tokens)
          if(haveTokenOffsets){
            Integer tokBegin = beginMap.get(charBegin);
            int searchStep = 1;          //if no exact match, search around the character offset
            while(tokBegin == null){
              tokBegin = beginMap.get(charBegin - searchStep);
              if(tokBegin == null){
                tokBegin = beginMap.get(charBegin + searchStep);

View on GitHub (pinned to 1b7edd19c4)