Skip to content

HttpServiceProxyFactory returns LinkedHashMap instead of RepresentationModel type for inherited method in CrudWebClient #36326

@aemini

Description

@aemini

Description

We are using Spring’s HTTP Interface support (@HttpExchange, HttpServiceProxyFactory) together with HATEOAS (RepresentationModel) to consume a Spring Data REST service.

We have defined a generic base interface:

public interface CrudWebClient<T, K extends Number> {

    @GetExchange
    PagedModel<T> list(@RequestParam @ModelAttribute Pageable pageable);

    @GetExchange(value="/{id}", accept=MediaTypes.VND_HAL_JSON_VALUE)
    T getById(@PathVariable K id);

    @PostExchange
    T create(@RequestBody T body);

    @PutExchange(value="/{id}")
    T update(@PathVariable K id, @RequestBody T body);

    @DeleteExchange(value="/{id}")
    void delete(@PathVariable K id);
}

This interface is intended to be extended by concrete entity clients.

Example:

@HttpExchange(url="/emails")
public interface EmailWebClient extends CrudWebClient<Email, Long> {

    @GetExchange(value="/{id}")
    Email foo(@PathVariable Long id);

    @GetExchange(value="/search/findAllByActor")
    CollectionModel<Email> findAllByActor(@RequestParam(value="actor") String actorSelfHref);

    @GetExchange(value="/search/findByEmailAddress")
    Email findByEmailAddress(@RequestParam(value="emailAddress") String emailAddress);

    @GetExchange(value="/{id}/actor")
    Actor getActor(@PathVariable Long id);
}

Email extends RepresentationModel<Email>.

The WebClient and proxy factory configuration:

@Configuration
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
public class WebClientConfig {

    @Bean
    public WebClient dataServiceWebClient(WebClient.Builder builder) {
        return builder
                .baseUrl(environment.getRequiredProperty("cloud.data-service.base-url"))
                .build();
    }

    @Bean
    public HttpServiceProxyFactory dataServiceProxyFactory(
            @Qualifier("dataServiceWebClient") WebClient webClient) {

        return HttpServiceProxyFactory.builder()
                .exchangeAdapter(WebClientAdapter.create(webClient))
                .build();
    }
}

Dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webclient</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

Controller Usage

@GetMapping("/emails/{id}")
public ResponseEntity<ApiResponse<Email>> getEmail(@PathVariable Long id) {

    var foo = emailWebClient.foo(id);
    System.out.println(foo.getEmailAddress());
    System.out.println(foo.getLink(IanaLinkRelations.SELF).orElseThrow().getHref());

    var email = emailWebClient.getById(id); // <-- problem occurs here

    return ResponseEntity.ok(SuccessResponse.of(email));
}

Observed Behavior

  • foo(id) works correctly and returns a properly deserialized Email object (including HATEOAS links).
  • getById(id) (inherited from CrudWebClient) does not return an Email.
  • Instead, the generated proxy returns a LinkedHashMap.
  • This leads to a ClassCastException or incorrect deserialization.

Expected Behavior

getById(id) should behave exactly like foo(id) and return a properly deserialized Email instance.

Notes

  • Both methods use @GetExchange("/{id}").
  • The only difference is that foo() is declared directly in EmailWebClient, while getById() is inherited from the generic parent interface.
  • Email extends RepresentationModel<Email>.
  • The issue appears to be related to generic type resolution in HttpServiceProxyFactory when the method is inherited from a parameterized parent interface.

Suspected Cause

It seems that Spring's HTTP interface proxy cannot correctly resolve the concrete generic type T from the parent interface (CrudWebClient<T, K>) at runtime.

As a result, the return type information is erased and deserialization falls back to LinkedHashMap.

This issue was reproduced on Spring Boot 4.0.2 and clearly indicates that generic return type resolution does not work correctly for inherited HTTP interface methods.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions