hibernate/hibernate-orm · error · AnnotationException
Write expression in '@ColumnTransformer' for property '${pro
Error message
Write expression in '@ColumnTransformer' for property '${propertyName}' and column '${logicalColumnName}' must contain exactly one placeholder character ('?') What it means
@ColumnTransformer(writeExpression = ...) supplies the SQL fragment Hibernate uses when binding a value for that column, and the expression must contain exactly one '?' placeholder which Hibernate substitutes with the bound parameter. AnnotatedColumn.bind counts '?' characters in the write expression during binding and throws this AnnotationException if the count is not exactly 1.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumn.java:324
mappingColumn.setUnique( unique );
// if the column name is not determined, we will assign the
// name to the unique key later this method gets called again
// from linkValueUsingDefaultColumnNaming() in second pass
if ( unique && nameDetermined ) {
// assign a unique key name to the column
getParent().getTable().createUniqueKey( mappingColumn, getBuildingContext() );
}
for ( var constraint : checkConstraints ) {
mappingColumn.addCheckConstraint( constraint );
}
mappingColumn.setDefaultValue( defaultValue );
mappingColumn.setOptions( options );
mappingColumn.setComment( comment );
if ( writeExpression != null ) {
final int numberOfJdbcParams = StringHelper.count( writeExpression, '?' );
if ( numberOfJdbcParams != 1 ) {
throw new AnnotationException(
"Write expression in '@ColumnTransformer' for property '" + propertyName
+ "' and column '" + logicalColumnName + "'"
+ " must contain exactly one placeholder character ('?')"
);
}
}
mappingColumn.setResolvedCustomRead( readExpression );
mappingColumn.setCustomWrite( writeExpression );
}
public boolean isNameDeferred() {
return mappingColumn == null || isEmpty( mappingColumn.getName() );
}
/**
* Attempt to infer the column name from the explicit {@code name} given by the annotation and the property or field
* name. In the case of a {@link jakarta.persistence.JoinColumn}, this is impossible, due to the rules implemented inView on GitHub (pinned to fad1729dce)
Solutions
- Rewrite the expression so it contains exactly one '?' - put constants inline and keep a single parameter slot, e.g. "upper(?)" or "my_function('const', ?)"
- If you need zero parameters (constant write), use @ColumnDefault or a generation strategy instead of @ColumnTransformer
- If you need multiple parameters, wrap them in a database function and call it with the single '?', e.g. "my_concat(?)"
- Check for stray '?' inside string literals and remove them
Example fix
// before: wrong placeholder counts
@ColumnTransformer(writeExpression = "concat(?, '-')") // ok (1), but:
@ColumnTransformer(writeExpression = "concat(?, ?, ?)") // 3 -> error
@ColumnTransformer(writeExpression = "now()") // 0 -> error
// after: exactly one placeholder per write expression
@ColumnTransformer(writeExpression = "concat(?, '-x')")
private String code;
// constant writes belong elsewhere
@ColumnDefault("now()")
// multi-arg logic belongs in a DB function called with the single bind
@ColumnTransformer(writeExpression = "my_transform(?)") Defensive patterns
Strategy: validation
Validate before calling
// Before boot / in unit tests: each write expression must contain exactly one '?'
static boolean writeExpressionsValid(Class<?> entity) {
for (Field f : entity.getDeclaredFields()) {
ColumnTransformer t = f.getAnnotation(ColumnTransformer.class);
if (t != null && !t.writeExpression().isEmpty()) {
long count = t.writeExpression().chars().filter(c -> c == '?').count();
if (count != 1) return false;
}
}
return true;
} Try / catch
try {
sessionFactory = metadata.buildSessionFactory();
} catch (AnnotationException e) {
if (e.getMessage().contains("must contain exactly one placeholder")) {
throw new IllegalStateException("@ColumnTransformer writeExpression needs exactly one '?'", e);
}
throw e;
} Prevention
- Treat the write expression as a single-parameter SQL template
- Push constants into the expression text and multi-value logic into database functions
- Unit-test annotation shapes on entities so typos fail before boot
When it happens
Trigger: A @ColumnTransformer writeExpression with zero placeholders (a constant expression like "upper('x')") or with two or more (like "? || '-' || ?"), including accidental '?' characters inside string literals of the expression.
Common situations: Writing encryption/computed-column expressions; copy-pasting SQL from a trigger definition that used multiple bind parameters; dialect-specific fragments ported from code that previously built SQL by hand.
Related errors
- '@AttributeAccessor' annotation must specify a 'strategy'
- Cannot perform #forceNotNull because internal org.hibernate.
- '@ColumnDefault' may only be applied to single-column mappin
- '@GeneratedColumn' may only be applied to single-column mapp
- '@Check' may only be applied to single-column mappings but '
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/4d5374364332a29e.
Report an issue: GitHub.