Skip to content

Feature/Support for QueryValue as a object #11787

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: 4.9.x
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ class ParameterBindingSpec extends AbstractMicronautSpec {
HttpMethod.GET | '/parameter/queryName/Fr%20ed' | "Parameter Value: Fr ed" | HttpStatus.OK
HttpMethod.POST | '/parameter/query?name=Fr%20ed' | "Parameter Value: Fr ed" | HttpStatus.OK
HttpMethod.GET | '/parameter/arrayStyle?param[]=a&param[]=b&param[]=c' | "Parameter Value: [a, b, c]" | HttpStatus.OK

HttpMethod.GET | '/parameter/query-object?age=30&title=JavaBook&author=JavaAuthor' | "Parameter Value: 30 JavaBook" | HttpStatus.OK
HttpMethod.GET | '/parameter/query-record?page=1&size=123' | "Parameter Value: 1 123" | HttpStatus.OK
}

void "test list to single error"() {
Expand Down Expand Up @@ -229,6 +232,16 @@ class ParameterBindingSpec extends AbstractMicronautSpec {
"Parameter Value: $params"
}

@Get('/query-object')
String queryObject(@QueryValue Book book) {
"Parameter Value: $book.age $book.title"
}

@Get('/query-record')
String queryRecord(@QueryValue PaginationRequest paginationRequest) {
"Parameter Value: $paginationRequest.page $paginationRequest.size"
}


@Introspected
static class Book {
Expand All @@ -250,7 +263,9 @@ class ParameterBindingSpec extends AbstractMicronautSpec {
int getAge() {
return age
}

}

@Introspected
record PaginationRequest(Integer page, Integer size) {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
package io.micronaut.http.bind.binders;

import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.beans.BeanIntrospection;
import io.micronaut.core.beans.BeanIntrospector;
import io.micronaut.core.beans.BeanProperty;
import io.micronaut.core.bind.annotation.AbstractArgumentBinder;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.format.Format;
Expand Down Expand Up @@ -91,6 +95,18 @@ public BindingResult<T> bind(ArgumentConversionContext<T> context, HttpRequest<?
return BindingResult.unsatisfied();
}

BindingResult<T> bindSimpleResult = bindSimple(context, source, annotationMetadata, parameters, argument);
if (bindSimpleResult.isSatisfied()) {
return bindSimpleResult;
}
return bindPojo(context, source, annotationMetadata, parameters, argument);
}

private BindingResult<T> bindSimple(ArgumentConversionContext<T> context,
HttpRequest<?> source,
AnnotationMetadata annotationMetadata,
ConvertibleMultiValues<String> parameters,
Argument<T> argument) {
// First try converting from the ConvertibleMultiValues type and if conversion is successful, return it.
// Otherwise, use the given uri template to deduce what to do with the variable
Optional<T> multiValueConversion;
Expand Down Expand Up @@ -130,6 +146,55 @@ public BindingResult<T> bind(ArgumentConversionContext<T> context, HttpRequest<?
return doBind(context, parameters, BindingResult.unsatisfied());
}

private BindingResult<T> bindPojo(ArgumentConversionContext<T> context,
HttpRequest<?> source,
AnnotationMetadata annotationMetadata,
ConvertibleMultiValues<String> parameters,
Argument<T> argument) {
Optional<BeanIntrospection<T>> introspectionOpt = BeanIntrospector.SHARED.findIntrospection(argument.getType());
if (introspectionOpt.isEmpty()) {
return BindingResult.unsatisfied();
}

BeanIntrospection<T> introspection = introspectionOpt.get();
Copy link
Contributor

@graemerocher graemerocher Apr 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be altered to use introspection.builder() so that records, immutable types, types with custom constructors etc. are supported.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using IntrospectionBuilder API and finally call the build() method any exception should b caught and the conversion rejected with context.reject

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the changes with the introspection.builder(). Seems to work for object with custom constructor but I made a test for record and don't work. Am I missing something?

BeanIntrospection.Builder<T> introspectionBuilder = introspection.builder();
Argument<?>[] builderArguments = introspectionBuilder.getBuilderArguments();

for (int index = 0; index < builderArguments.length; index++) {
Argument<?> builderArg = builderArguments[index];
String propertyName = builderArg.getName();
Optional<String> value = parameters.getFirst(propertyName);

if (value.isEmpty()) {
value = builderArg
.getAnnotationMetadata()
.stringValue(Bindable.class, "defaultValue");
}

if (value.isPresent()) {
Optional<?> converted = conversionService.convert(value.get(), context.with(builderArg));
if (converted.isPresent()) {
try {
@SuppressWarnings({"unchecked"})
Argument<Object> rawArg = (Argument<Object>) builderArg;
introspectionBuilder.with(index, rawArg, converted.get());
} catch (Exception e) {
context.reject(builderArg, e);
return BindingResult.unsatisfied();
}
}
}
}

try {
T instance = introspectionBuilder.build();
return () -> Optional.of(instance);
} catch (Exception e) {
context.reject(argument, e);
return BindingResult.unsatisfied();
}
}

@Override
protected String getParameterName(Argument<T> argument) {
return argument.getAnnotationMetadata().stringValue(QueryValue.class).orElse(argument.getName());
Expand Down