A TextField stored in a numeric column makes the whole contentlet unindexable
@fabrizzio-dotCMS is already working on this.
Since Aug 28, 2026.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Problem Statement
ESMappingAPIImpl.loadFields() decides how to serialize a field by its storage column
(field_contentlet) rather than its field type. When a TextField is backed by an integerN
or floatN column — which dotCMS allows and which exists in real content types — the String value
is handed to DecimalFormat.format() and throws:
WARN business.ESMappingAPIImpl - Error indexing field: viewWeekTotal of contentlet: 3c3276a0-33d2-4c5e-80a8-09edeb11572f
java.lang.IllegalArgumentException: Cannot format given Object as a Number
at java.base/java.text.DecimalFormat.format(DecimalFormat.java:584)
at java.base/java.text.Format.format(Format.java:165)
at com.dotcms.content.elasticsearch.business.ESMappingAPIImpl.loadFields(ESMappingAPIImpl.java:1118)
at com.dotcms.content.elasticsearch.business.ESMappingAPIImpl.toMap(ESMappingAPIImpl.java:465)
at com.dotcms.content.elasticsearch.business.ContentletIndexAPIImpl.addBulkRequest(ContentletIndexAPIImpl.java:2785)
The affected fields:
| Content Type | Field | field_type |
field_contentlet |
|---|---|---|---|
RptTopHits |
viewWeekTotal |
com.dotcms.contenttype.model.field.TextField |
integer1 |
Trending |
viewWeekTotal |
com.dotcms.contenttype.model.field.TextField |
integer1 |
Stored value: {"type": "Text", "value": "54"} — a String, correctly matching the declared
TextField. But the branch is chosen by the column name:
// ESMappingAPIImpl.java:1114
} else if (field.getFieldContentlet()
.startsWith(ESMappingConstants.FIELD_ELASTIC_TYPE_FLOAT) || field
.getFieldContentlet()
.startsWith(ESMappingConstants.FIELD_ELASTIC_TYPE_INTEGER)) {
contentletMap.put(keyName, valueObj);
contentletMap.put(keyNameText, numFormatter.format(valueObj)); // String -> IllegalArgumentException
}
integer1 starts with integer, so the numeric branch is taken for a field whose type and value
are both textual.
The error handling contradicts itself
// ESMappingAPIImpl.java:1141
} catch (Exception e) {
Logger.warn(ESMappingAPIImpl.class, "Error indexing field: " + field.getFieldName()
+ " of contentlet: " + contentlet.getInode(), e);
throw new DotDataException(e.getMessage(), e);
}
It logs at WARN — the level that says "recoverable, carry on" — and then rethrows, aborting the
entire contentlet. One misconfigured field out of dozens makes the whole document unindexable. The
adjacent Date branch a few lines above does the opposite and degrades gracefully:
} catch(Exception ex) {
contentletMap.put(keyName, valueObj);
contentletMap.put(keyNameText, valueObj.toString());
}
A valueObj.toString() fallback here would have indexed "54" correctly, since that is what the
field declares itself to be.
How it surfaced
Found while reindexing 370 contentlets via POST /api/v1/content/_bulkrefresh: 364 succeeded, 6
failed — 4 from an unrelated serialization limit and these 2. Both are RptTopHits/Trending
content, so any reindex touching those types loses those documents. The failure is silent from the
operator's side: the UI reports a flat failure count with no field attribution (see the companion
issue on per-engine and per-document failure visibility).
Steps to Reproduce
-
Create a content type with a Text field whose backing column is numeric. On an existing
install this can be confirmed with:SELECT st.velocity_var_name AS content_type, f.velocity_var_name AS field, f.field_type, f.field_contentlet FROM field f JOIN structure st ON st.inode = f.structure_inode WHERE f.field_type LIKE '%TextField' AND (f.field_contentlet LIKE 'integer%' OR f.field_contentlet LIKE 'float%'); -
Save a contentlet with a textual value in that field (e.g.
"54"). -
Reindex it — full reindex,
POST /api/v1/esindex/reindex?contentType=<type>, or
POST /api/v1/content/_bulkrefresh. -
Observe
Cannot format given Object as a Numberand the contentlet absent from the index. -
Note the value is a perfectly valid
TextFieldvalue; nothing in the UI flags the field or
the content type as invalid.
Acceptance Criteria
-
loadFields()selects the serialization branch by field type, not by storage column;
or the numeric branch tolerates non-numeric values. - A field that cannot be serialized does not abort the whole contentlet — it is skipped (or
indexed viatoString()) and the rest of the document is indexed, consistent with how the
Datebranch already behaves. - If a field is genuinely unindexable, the reported failure names the field, not just the
contentlet, so the content-type misconfiguration is actionable. - The log level matches the outcome: if the document is aborted it is an ERROR, not a WARN.
- Consider validating type/column consistency when a content type is saved, so this cannot be
modelled in the first place.
dotCMS Version
dotcms/dotcms:trunk. PostgreSQL 16, ~1.55 M contentlets, OpenSearch 1.3.20 + 3.4.0 in
PHASE_1_DUAL_WRITE_ES_READS.
Severity
Medium - Some functionality impacted
Medium — affects only content types with this modelling, but for those every document is silently
missing from the index, and the cause is not discoverable from the UI.
Links
ESMappingAPIImpl.java:1114— branch selected byfield_contentletinstead of field typeESMappingAPIImpl.java:1118—numFormatter.format(valueObj)ESMappingAPIImpl.java:1141— WARN followed by rethrowESMappingAPIImpl.java:1121— theDatebranch that degrades gracefully instead
Freshdesk ticket: NA — found during internal ES→OpenSearch migration testing.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.