hibernate/hibernate-orm · error · QueryException

H2 json_table() only supports literal default expressions, b

Error message

H2 json_table() only supports literal default expressions, but got {defaultExpression}

What it means

In the H2 json_table() emulation, a column-level default <expr> on empty clause is implemented by baking the default value into the generated coalesce/cast SQL text at translation time. Only compile-time Literal values can be baked in; a bind parameter or computed expression as the default is rejected with this QueryException while the column mappings are built.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonTableFunction.java:709

			}
			return addSelectableMappings( selectableMappings, tableIdentifierVariable, columnDefinition.columns(), nextClauseLevel, readExpression, converter );
		}

		protected void addSelectableMappings(List<SelectableMapping> selectableMappings, String tableIdentifierVariable, JsonTableOrdinalityColumnDefinition definition, int clauseLevel, SqmToSqlAstConverter converter) {
			addSelectableMapping(
					selectableMappings,
					definition.name(),
					ordinalityExpression( tableIdentifierVariable, clauseLevel ),
					converter.getCreationContext().getTypeConfiguration().getBasicTypeForJavaType( Long.class )
			);
		}

		protected void addSelectableMappings(List<SelectableMapping> selectableMappings, JsonTableValueColumnDefinition definition, String parentReadExpression, SqmToSqlAstConverter converter) {
			final JsonValueEmptyBehavior emptyBehavior = definition.emptyBehavior();
			final Literal defaultExpression;
			if ( emptyBehavior != null && emptyBehavior.getDefaultExpression() != null ) {
				if ( !( emptyBehavior.getDefaultExpression() instanceof Literal literal ) ) {
					throw new QueryException( "H2 json_table() only supports literal default expressions, but got " + emptyBehavior.getDefaultExpression() );
				}
				defaultExpression = literal;
			}
			else {
				defaultExpression = null;
			}
			final String baseReadExpression = determineElementReadExpression( definition.name(), definition.jsonPath(), parentReadExpression );
			final String elementReadExpression = castValueExpression( baseReadExpression, definition.type(), defaultExpression, converter );

			addSelectableMapping(
					selectableMappings,
					definition.name(),
					elementReadExpression,
					definition.type().getJdbcMapping()
			);
		}

		private String castValueExpression(String baseReadExpression, CastTarget castTarget, @Nullable Literal defaultExpression, SqmToSqlAstConverter converter) {

View on GitHub (pinned to fad1729dce)

Solutions

  1. Use a literal default: columns(name varchar default 'N/A' on empty)
  2. Apply the variable default outside the function: select coalesce(t.name, :fallback) from ... json_table(...) t
  3. Default to null inside json_table and substitute the default in application code
  4. Fall back to a native query if the default must be a computed expression

Example fix

// before - throws on H2 (non-literal default expression)
select t.name from Document d, json_table(d.doc, '$.items[*]' columns(name varchar default :fallback on empty)) t

// after - literal default inside, variable default applied with coalesce outside
select coalesce(t.name, :fallback) from Document d, json_table(d.doc, '$.items[*]' columns(name varchar default 'N/A' on empty)) t
Defensive patterns

Strategy: validation

Validate before calling

// Heuristic lint: json_table column defaults must be literals on H2
private static final Pattern NONLITERAL_DEFAULT =
    Pattern.compile("(?i)default\\s+(:\\w+|[a-z_]+\\s*\\()");

static void assertLiteralDefaults(String hql) {
    if (NONLITERAL_DEFAULT.matcher(hql).find()) {
        throw new IllegalArgumentException(
            "json_table column defaults must be literals on H2; apply variable defaults with coalesce() outside");
    }
}

Type guard

// Java predicate (type-guard analogue) for the default expression node
static boolean isLiteralDefault(org.hibernate.sql.ast.tree.expression.Expression defaultExpr) {
    return defaultExpr instanceof org.hibernate.sql.ast.tree.expression.Literal;
}

Try / catch

try {
    return session.createQuery(hql, Object[].class).getResultList();
} catch (org.hibernate.QueryException e) {
    if (e.getMessage() != null && e.getMessage().contains("only supports literal default expressions")) {
        // Rewrite: move the dynamic default outside the json_table call
        throw new IllegalArgumentException(
            "Use a literal default or coalesce(t.col, :fallback) instead", e);
    }
    throw e;
}

Prevention

When it happens

Trigger: HQL on H2 with a non-literal column default: json_table(d.doc, '$.items[*]' columns(name varchar default :fallback on empty)) or columns(name varchar default concat('n','a') on empty). emptyBehavior.getDefaultExpression() is not a Literal, so addSelectableMappings throws.

Common situations: Wanting per-request default values for missing JSON fields; the same query rendering fine on dialects that inline defaults as ordinary SQL expressions, then failing on the H2 test profile.

Related errors


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