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 18 commits into
base: 4.9.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8f061b2
gh-11473: expand QueryValueArgumentBinder to binding POJO
MarianConstantinMarica Apr 26, 2025
90cdb0a
gh-11473: create a test for endpoint in ParameterBindingSpec
MarianConstantinMarica Apr 27, 2025
10ed6d2
gh-11473: fix issue with bindSimple
MarianConstantinMarica Apr 27, 2025
1c985b7
gh-11473: fix issue with bindPojo
MarianConstantinMarica Apr 28, 2025
cac5b32
gh-11473: add empty constructor in Book pojo and setters
MarianConstantinMarica Apr 28, 2025
f0fb8f7
gh-11473: remove unused imports
MarianConstantinMarica Apr 28, 2025
2513fda
gh:11473: use introspection.builder() so that records, immutable type…
MarianConstantinMarica Apr 29, 2025
a1745b4
gh-11473: create a test for record
MarianConstantinMarica Apr 29, 2025
6e9a62f
gh-11473: use getBuilderArguments()
MarianConstantinMarica May 4, 2025
88ddb6a
fix tests
graemerocher Jun 9, 2025
797c29f
projectVersion=4.10.0-SNAPSHOT [ci skip]
sdelamo Jun 10, 2025
21dd30a
fix(deps): update managed.reactor to v3.7.7 (#11850)
renovate[bot] Jun 11, 2025
77e0a29
Allow visiting imported classes + Mixin (#11845)
dstepanov Jun 11, 2025
3a7cccf
Merge branch '4.9.x' into 4.10.x-upd
yawkat Jun 14, 2025
ed0da60
Merge pull request #11866 from micronaut-projects/4.10.x-upd
graemerocher Jun 15, 2025
b816e29
fix(deps): update vertx to v4.5.15 (#11868)
renovate[bot] Jun 17, 2025
9f096ad
fix(deps): update bytebuddy to v1.17.6 (#11877)
renovate[bot] Jun 18, 2025
d11b231
Merge branch 'micronaut-projects:4.10.x' into feature/queryvalue-as-o…
MarianConstantinMarica Jun 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
projectVersion=4.9.2-SNAPSHOT
projectVersion=4.10.0-SNAPSHOT
projectGroupId=io.micronaut
projectDesc=Core components supporting the Micronaut Framework
title=Micronaut Core
Expand Down
6 changes: 3 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ awaitility = "4.3.0"
bcpkix = "1.70"
blaze = "1.6.15"
brotli4j = "1.18.0"
bytebuddy = "1.17.5"
bytebuddy = "1.17.6"
caffeine = "2.9.3"
classgraph = "4.8.179"
compile-testing = "0.21.0"
Expand Down Expand Up @@ -60,7 +60,7 @@ spotbugs = "4.7.1"
systemlambda = "1.2.1"
testcontainers = "1.21.1"
tomlj="1.1.1"
vertx = "4.5.14"
vertx = "4.5.15"
wiremock = "2.33.2"
mimepull = "1.10.0"
micronaut-sourcegen = "1.7.10"
Expand All @@ -84,7 +84,7 @@ managed-netty = "4.2.2.Final"
managed-netty-tcnative = "2.0.72.Final"
managed-reactive-streams = "1.0.4"
# This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM
managed-reactor = "3.7.6"
managed-reactor = "3.7.7"
managed-snakeyaml = "2.4"
managed-java-parser-core = "3.26.4"
managed-ksp = "1.9.25-1.0.20"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.micronaut.http.server.netty.binding;

import io.micronaut.core.annotation.Introspected;

@Introspected
public record PaginationRequest(Integer page, Integer size) {}
Copy link
Contributor

Choose a reason for hiding this comment

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

@MarianConstantinMarica just rename PaginationRequest.java to PaginationRequest.groovy and after this all must work correctly

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 All @@ -105,7 +108,6 @@ class ParameterBindingSpec extends AbstractMicronautSpec {

expect:
response.status() == HttpStatus.BAD_REQUEST
response.body().contains('Unexpected token (VALUE_STRING), expected END_ARRAY')
}

@Requires(property = 'spec.name', value = 'ParameterBindingSpec')
Expand Down Expand Up @@ -229,6 +231,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 +262,7 @@ class ParameterBindingSpec extends AbstractMicronautSpec {
int getAge() {
return age
}

}

}
}
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.annotation.Nullable;
import io.micronaut.core.beans.BeanIntrospection;
import io.micronaut.core.beans.BeanIntrospector;
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 All @@ -28,6 +32,7 @@
import io.micronaut.http.uri.UriMatchVariable;

import java.util.Collections;
import java.util.List;
import java.util.Optional;

/**
Expand All @@ -53,7 +58,7 @@ public QueryValueArgumentBinder(ConversionService conversionService) {
* Constructor.
*
* @param conversionService conversion service
* @param argument The argument
* @param argument The argument
*/
public QueryValueArgumentBinder(ConversionService conversionService, Argument<T> argument) {
super(conversionService, argument);
Expand Down Expand Up @@ -91,6 +96,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, 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 +147,50 @@ public BindingResult<T> bind(ArgumentConversionContext<T> context, HttpRequest<?
return doBind(context, parameters, BindingResult.unsatisfied());
}

private BindingResult<T> bindPojo(ArgumentConversionContext<T> context,
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?

Copy link
Contributor

Choose a reason for hiding this comment

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

this is because records are not supported in Groovy and you a rewriting the test in Groovy. Probably will be fixed by #11832

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();
List<String> values = parameters.getAll(propertyName);
boolean hasNoValue = values.isEmpty();
@Nullable String defaultValue = hasNoValue ? builderArg
.getAnnotationMetadata()
.stringValue(Bindable.class, "defaultValue").orElse(null) : null;

ArgumentConversionContext<?> conversionContext = context.with(builderArg);
Optional<?> converted = hasNoValue ? conversionService.convert(defaultValue, conversionContext) :conversionService.convert(values, conversionContext);
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