I hit this issue and after digging into it with Claude, I had it generate the following description:
Summary
When a request payload property is a String (or any CharSequence) annotated with jakarta.validation.constraints.@Size, the generated HAL-FORMS template property gets type: "range" and the bounds are emitted as min/max. For a string-length constraint this is wrong on two counts per the HAL-FORMS spec:
type should be a text input ("text"), not "range" (which denotes a numeric slider).
- the bounds should be
minLength/maxLength (character-count constraints), not min/max (numeric-value constraints).
@Size in Jakarta Bean Validation constrains the element/character count of a CharSequence/Collection/Map/array — it is not a numeric-value range — so mapping it to range + min/max is semantically incorrect.
Versions
- Spring HATEOAS: 3.0.3
- Media type:
application/prs.hal-forms+json
Steps to reproduce
class CreateUserRequest {
@Size(max = 256)
private String name;
// getter/setter
}
// controller method create(CreateUserRequest) exposed via:
Affordances.of(link).afford(HttpMethod.POST)
.withInput(CreateUserRequest.class) // or afford(methodOn(Ctrl.class).create(null))
...
Actual HAL-FORMS property:
{ "name": "name", "type": "range", "min": 0, "max": 256, "required": false }
Expected HAL-FORMS property:
{ "name": "name", "type": "text", "minLength": 0, "maxLength": 256, "required": false }
Root cause
In org.springframework.hateoas.mediatype.PropertyUtils.Jsr303AwarePropertyMetadata:
-
The static TYPE_MAP maps Size.class to "range", so getInputType() → lookupFromTypeMap() returns "range" for any @Size property:
typeMap.put(Size.class, "range");
-
getMin() / getMax() read the min/max attributes of @Size and surface them as the numeric min/max of the property.
-
getMinLength() / getMaxLength() are populated only from Hibernate's org.hibernate.validator.constraints.@Length — never from @Size:
public Long getMinLength() {
return LENGTH_ANNOTATION.flatMap(it -> getAnnotationAttribute(it, "min", Integer.class))...
}
So for the standard, framework-agnostic @Size annotation there is no way to produce minLength/maxLength, and the input type is forced to range.
Impact
- Clients that honor
type render a numeric range/slider control for a free-text field.
- Spec-conformant clients that read length limits from
minLength/maxLength never see the constraint, since it is emitted under min/max (which apply to numeric value, not character count, on a text field). The @Size cap therefore isn't advertised in a form clients can apply.
Workarounds (and why they're partial)
@org.springframework.hateoas.InputType("text") fixes type but leaves the bounds under min/max (no minLength/maxLength), because getMin()/getMax() still read @Size regardless of the declared input type.
- Hibernate's
@Length is currently the only annotation that yields minLength/maxLength — but it couples the API model to a Hibernate-specific annotation.
Suggested fix
For CharSequence-typed properties, map @Size to minLength/maxLength rather than min/max, and don't force the input type to range (let it default to text). Reserve range + min/max for genuinely numeric constraints (@Min/@Max/@DecimalMin/@DecimalMax/Hibernate @Range). (@Size on a Collection/Map/array is a separate question — HAL-FORMS has no array-length attribute on plain properties — but the CharSequence case is unambiguous and by far the most common.)
I hit this issue and after digging into it with Claude, I had it generate the following description:
Summary
When a request payload property is a
String(or anyCharSequence) annotated withjakarta.validation.constraints.@Size, the generated HAL-FORMS template property getstype: "range"and the bounds are emitted asmin/max. For a string-length constraint this is wrong on two counts per the HAL-FORMS spec:typeshould be a text input ("text"), not"range"(which denotes a numeric slider).minLength/maxLength(character-count constraints), notmin/max(numeric-value constraints).@Sizein Jakarta Bean Validation constrains the element/character count of aCharSequence/Collection/Map/array — it is not a numeric-value range — so mapping it torange+min/maxis semantically incorrect.Versions
application/prs.hal-forms+jsonSteps to reproduce
Actual HAL-FORMS property:
{ "name": "name", "type": "range", "min": 0, "max": 256, "required": false }Expected HAL-FORMS property:
{ "name": "name", "type": "text", "minLength": 0, "maxLength": 256, "required": false }Root cause
In
org.springframework.hateoas.mediatype.PropertyUtils.Jsr303AwarePropertyMetadata:The static
TYPE_MAPmapsSize.classto"range", sogetInputType()→lookupFromTypeMap()returns"range"for any@Sizeproperty:getMin()/getMax()read themin/maxattributes of@Sizeand surface them as the numericmin/maxof the property.getMinLength()/getMaxLength()are populated only from Hibernate'sorg.hibernate.validator.constraints.@Length— never from@Size:So for the standard, framework-agnostic
@Sizeannotation there is no way to produceminLength/maxLength, and the input type is forced torange.Impact
typerender a numeric range/slider control for a free-text field.minLength/maxLengthnever see the constraint, since it is emitted undermin/max(which apply to numeric value, not character count, on a text field). The@Sizecap therefore isn't advertised in a form clients can apply.Workarounds (and why they're partial)
@org.springframework.hateoas.InputType("text")fixestypebut leaves the bounds undermin/max(nominLength/maxLength), becausegetMin()/getMax()still read@Sizeregardless of the declared input type.@Lengthis currently the only annotation that yieldsminLength/maxLength— but it couples the API model to a Hibernate-specific annotation.Suggested fix
For
CharSequence-typed properties, map@SizetominLength/maxLengthrather thanmin/max, and don't force the input type torange(let it default totext). Reserverange+min/maxfor genuinely numeric constraints (@Min/@Max/@DecimalMin/@DecimalMax/Hibernate@Range). (@Sizeon aCollection/Map/array is a separate question — HAL-FORMS has no array-length attribute on plain properties — but theCharSequencecase is unambiguous and by far the most common.)