hibernate/hibernate-orm · error · IllegalArgumentException

XML starts sub-object for a non-aggregate type at index %d.

Error message

XML starts sub-object for a non-aggregate type at index %d. Selectable [%s] is of type [%s]

What it means

While parsing an XML aggregate into an embeddable, Hibernate met a nested start tag for a selectable whose JdbcType is not an AggregateJdbcType. The stored data contains a sub-object where the current mapping declares a basic scalar, so element-wise parsing cannot proceed.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlHelper.java:544

								final int end = fromArrayString(
										string,
										returnEmbeddable,
										options,
										i,
										arrayList,
										elementType.getMappedJavaType(),
										elementType.getJdbcJavaType(),
										elementType.getJdbcType()
								);
								values[selectableIndex] = selectable.getJdbcMapping().getJdbcJavaType().wrap( arrayList, options );
								// The end is the start angle bracket for the end tag
								assert string.charAt( end ) == '<';
								assert string.charAt( end + 1 ) == '/';
								assert string.regionMatches( end + 2, tagName, 0, tagName.length() );
								i = end;
							}
							else {
								throw new IllegalArgumentException(
										String.format(
												"XML starts sub-object for a non-aggregate type at index %d. Selectable [%s] is of type [%s]",
												i,
												selectable.getSelectableName(),
												jdbcType.getClass().getName()
										)
								);
							}
						}
						// consume the whole closing tag
						i += tagName.length() + 2;
						tagNameStart = -1;
						contentStart = -1;
						tagName = null;
					}
					break;
				case '>':
					if ( tagName == null ) {

View on GitHub (pinned to fad1729dce)

Solutions

  1. Align the mapping with the stored shape: re-introduce the nested embeddable the data still has.
  2. Or migrate the column data to the new flat shape before deploying the new mapping.
  3. Version the aggregate layout and migrate rows on every layout change.

Example fix

// before: mapping declares a basic field, data has <address><city>..</city></address>
String city;
// after: mapping matches the stored nested object
@Embeddable
static class Address { String city; }
Address address;
Defensive patterns

Strategy: validation

Validate before calling

// before deploying a refactored embeddable, compare stored tags with the mapping
Set<String> storedTags = extractChildTags(jdbc, "select xml_col from t");
Set<String> mappedTags = expectedTagsFor(Details.class); // field/@Column names
if (!mappedTags.containsAll(storedTags) || !storedTags.containsAll(mappedTags)) {
    throw new IllegalStateException("XML aggregate layout drift: " 
        + symmetricDifference(storedTags, mappedTags));
}

Try / catch

try {
    return session.find(Person.class, id);
} catch (IllegalArgumentException ex) {
    if (ex.getMessage() != null && ex.getMessage().contains("XML starts sub-object")) {
        // row layout is newer/older than the deployed mapping
        quarantine(id, ex.getMessage());
        return null;
    }
    throw ex;
}

Prevention

When it happens

Trigger: Loading an entity after the embeddable was refactored: a nested embeddable flattened to a basic field (or a field moved between nesting levels) while stored rows still contain the nested tags. Also data written by a different mapping where the field was an aggregate.

Common situations: Schema/refactor drift between application versions; sharing one XML column between two mappings; restoring old backups into a new schema.

Related errors


AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22). Data as JSON: /api/errors/dd9905636609e4d6. Report an issue: GitHub.