quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.56k stars 2.62k forks source link

Resteasy Reactive doesn't fully recognize the bean param when it's in generic interface #42807

Open tran4774 opened 2 weeks ago

tran4774 commented 2 weeks ago

Describe the bug

When I use a generic interface defined for the controller, the @BeanParam doesn't fully recognize the query param defined in the class.

I have 2 models (called filters): PageFilter is the base filter and UserFilter extends it

public abstract class PageFilter {
    @RestQuery
    @DefaultValue(value = "0")
    protected Integer number;
    @RestQuery
    @DefaultValue(value = "20")
    protected Integer size;
    @RestQuery
    private List<String> sort;
}
public class UserFilter extends PageFilter {
    @RestQuery
    private String username;
    @RestQuery
    private String fullName;
}

And I have a generic interface like this

public interface IGetInfoPageController<ID, T extends BaseData<ID>, F extends PageFilter> {

    @GET
    @Path("/list")
    @Produces(MediaType.APPLICATION_JSON)
    default RestResponse<BaseResponse<BasePagingResponse<T>>> getInfoPageWithFilter(@BeanParam @Valid F filter) {
        // some logic code
    }
}

I implement that in the controller:

@Path("/api/user")
@Tag(name = "User Controller")
public class UserController implements IGetInfoPageController<String, UserInfo, UserFilter> {
   //Some method and logic
}

I want a bean param UserFilter containing 5 fields: number, size, sort, username, and fullName. But username and fullName are always null. When I checked OpenAPI UI, it only showed 3 fields in PageFilter

Expected behavior

No response

Actual behavior

No response

How to Reproduce?

No response

Output of uname -a or ver

Linux 6.8.0-41-generic #41-Ubuntu SMP PREEMPT_DYNAMIC Fri Aug 2 20:41:06 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

Output of java -version

java 17.0.8 2023-07-18 LTS Java(TM) SE Runtime Environment Oracle GraalVM 17.0.8+9.1 (build 17.0.8+9-LTS-jvmci-23.0-b14) Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 17.0.8+9.1 (build 17.0.8+9-LTS-jvmci-23.0-b14, mixed mode, sharing)

Quarkus version or git rev

3.13.3

Build tool (ie. output of mvnw --version or gradlew --version)

Gradle 8.8

Additional information

No response

quarkus-bot[bot] commented 2 weeks ago

/cc @FroMage (resteasy-reactive), @geoand (resteasy-reactive), @stuartwdouglas (resteasy-reactive)

geoand commented 2 weeks ago

I'm sure @FroMage is going to love this one 😄

tran4774 commented 2 weeks ago

Any fix for this issue? I have tried quarkus-spring-web and it still occur

nasonawa commented 2 weeks ago

Hi @tran4774,

i reproduced the issue at my end, seems like fullName and username are not shown in the OpenAPI UI, but when i tried with the from "curl" command it was working for me curl -X 'GET' 'http://localhost:8080/api/user/list?fullName=123213&number=123&size=20&sort=string&sort=string&username=123123' -H 'accept: text/plain

FroMage commented 2 weeks ago

TBH I'm even surprised Quarkus REST works with generic methods like these. I don't recall adding support for that, so it must have been someone else.

Not surprised bean params or openapi doesn't work with this. It requires quite some generics knowledge to get it to work.

And it could be worse: the bean param classes don't use generics, that wouldn't work either.

Anyway, I definitely won't have time to fix this soon, sorry, but I can help you if you can provide a PR.

Also, as @nasonawa said, it's fairly possible this actually works in Quarkus REST, and this is just openapi not supporting this.

But it's going to be tricky to verify this without actually reifying the method, since you can't access the extra fields in the interface:

public interface IGetInfoPageController<ID, T extends BaseData<ID>, F extends PageFilter> {

    @GET
    @Path("/list")
    @Produces(MediaType.APPLICATION_JSON)
    default RestResponse<BaseResponse<BasePagingResponse<T>>> getInfoPageWithFilter(@BeanParam @Valid F filter) {
        // here you can only access filter.username by downcasting
    }
}

@Path("/api/user")
@Tag(name = "User Controller")
public class UserController implements IGetInfoPageController<String, UserInfo, UserFilter> {
   //Some method and logic
}

If you start overriding getInfoPageWithFilter in UserController I suspect it will start working, since you'd be reifying the type argument.

Apparently, you said:

But username and fullName are always null

Which means you did test it and it did not set the extra fields, which leads me to suspect that it's the parameter injector which is wrong in not applying type arguments. I'm sure we're looking up an argument of type PageFilter from CDI to fill the bean param.

I'm saying this because I'm pretty sure the bean params injection will be generated properly (there's no generics there), so if we passed a UserFilter instance, it would automatically get filled up. So we're probably passing a PageFilter.

You can check this by printing the class of the @BeanParam @Valid F filter argument. Let us know?

tran4774 commented 2 weeks ago

I have tried curl way before (like @nasonawa) but it's still null. Here is my code after adding the log

public interface IGetInfoPageController<ID, T extends BaseData<ID>, F extends PageFilter> {

    @GET
    @Path("/list")
    @Produces(MediaType.APPLICATION_JSON)
    default RestResponse<BaseResponse<BasePagingResponse<T>>> getInfoPageWithFilter(@BeanParam @Valid F filter) {
        log.info("Bean param type: {}", filter.getClass().getName());
        log.info("Bean param: {}", Json.encode(filter));
        // some logic
    }
}

The curl command is

curl -X 'GET' \
  'http://localhost:8080/api/user/list?number=0&size=20&username=123123&fullName=123213' \
  -H 'accept: application/json'

And here is my log image @FroMage If I can create PR, where is the place you resolve bean params?

FroMage commented 2 weeks ago

Ah, so I'm wrong and we're making the proper UserFilter, I'll have to take a look in the debugger to see what goes wrong, then.

nasonawa commented 1 week ago

Hi @FroMage and @tran4774 sharing my working code and output just for your reference,

Screenshot from 2024-09-02 10-53-25

public interface IGetInfoPageController<ID, F extends PageFilter> {

    @GET
    @Path("/list")
    @Produces(MediaType.TEXT_PLAIN)
    default String getInfoPageWithFilter(@BeanParam @Valid F filter){
        System.out.println(filter.getClass().getName());
        System.out.println(filter);
        return "Hello";
    }
}

The only difference is that i am returning plain text response and i think that should not affect how bean param works.