Skip to content

@Size on String properties renders as HAL-FORMS type: "range" with min/max instead of a text input with minLength/maxLength #2531

@cpoulsen-dezide

Description

@cpoulsen-dezide

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.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions