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 in

View on GitHub (pinned to fad1729dce)

Solutions

  1. Rewrite the expression so it contains exactly one '?' - put constants inline and keep a single parameter slot, e.g. "upper(?)" or "my_function('const', ?)"
  2. If you need zero parameters (constant write), use @ColumnDefault or a generation strategy instead of @ColumnTransformer
  3. If you need multiple parameters, wrap them in a database function and call it with the single '?', e.g. "my_concat(?)"
  4. 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

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


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