I came up with an answer. It's not necessarily the best answer and I would love to be corrected if any Springdoc experts see this, but it's the best I could do with the time I had for this problem.
After some reverse engineering, my estimation is that Springdoc does not support this or almost support this; i.e., a simple config change will not make it "just work".
Springdoc does have a QuerydslPredicateOperationCustomizer
that supports Querydsl predicates in a similar fashion to what I'm asking, but it is triggered by the @QuerydslPredicate
annotation and relies on domain-specific configuration on the annotation, which is not available for the annotated RootResourceInformation
argument in Spring Data REST's RepositoryEntityController
. It also only gets invoked when adequate operation context is provided, which Springdoc does not include for Spring Data REST's endpoints (perhaps for no other reason than that doing so breaks the QuerydslPredicateOperationCustomizer
- I'm not sure). Long story short, this customizer doesn't work for this use case.
Ideally, this should probably be fixed within the QuerydslPredicateOperationCustomizer
, but that is made more difficult than it should be by the fact that the DataRestRepository
context is not available in that scope, which would be the simplest path to the entity domain type from which parameters could be inferred. Instead, the available context refers to the handler method within the RepositoryEntityController
, which is generic to all entities and yields no simple way of inferring domain types.
To make this work at that level, the customizer would have to redo the process of looking up the domain type from the limited context that is available (which seems hard to implement without brittleness), or perhaps preferably, additional metadata would need to be carried throughout the process up to this point.
Any of that would require more expertise with Springdoc than I have, plus buy-in from Springdoc's development team. If any of them see this and have interest in an enhancement to this end, I would be happy to lend the knowledge I have of these integrations.
I extended Springdoc's DataRestRequestService
with a mostly-identical service that I marked as the @Primary
bean of its type, thus replacing the component used by Springdoc. In its buildParameters
method, I added the line buildCustomParameters(operation, dataRestRepository);
which invoked the methods below. It's imperfect to be sure, but it worked well enough for my purposes (which was mainly about being able to use OpenAPI Generator to generate a fully functional SDK for my API).
public void buildCustomParameters(Operation operation, DataRestRepository dataRestRepository) {
if (operation.getOperationId().startsWith("getCollectionResource-")) {
addProjectionParameter(operation);
addQuerydslParameters(operation, dataRestRepository.getDomainType());
} else if (operation.getOperationId().startsWith("getItemResource-")) {
addProjectionParameter(operation);
}
}
public void addProjectionParameter(Operation operation) {
var projectionParameter = new Parameter();
projectionParameter.setName("projection");
projectionParameter.setIn("query");
projectionParameter.setDescription(
"The name of the projection to which to cast the response model");
projectionParameter.setRequired(false);
projectionParameter.setSchema(new StringSchema());
addParameter(operation, projectionParameter);
}
public void addQuerydslParameters(Operation operation, Class<?> domainType) {
var queryType = SimpleEntityPathResolver.INSTANCE.createPath(domainType);
var pathInits =
Arrays.stream(queryType.getClass().getDeclaredFields())
.filter(field -> Modifier.isStatic(field.getModifiers()))
.filter(field -> PathInits.class.isAssignableFrom(field.getType()))
.findFirst()
.flatMap(
field -> {
try {
field.setAccessible(true);
return Optional.of((PathInits) field.get(queryType));
} catch (Throwable ex) {
return Optional.empty();
}
})
.orElse(PathInits.DIRECT2);
var paths = getPaths(queryType.getClass(), pathInits);
var parameters =
paths.stream()
.map(
path -> {
var parameter = new Parameter();
parameter.setName(path);
parameter.setIn("query");
parameter.setRequired(false);
return parameter;
})
.toArray(Parameter[]::new);
addParameter(operation, parameters);
}
protected Set<String> getPaths(Class<?> clazz, PathInits pathInits) {
return getPaths(clazz, "", pathInits).collect(Collectors.toSet());
}
protected Stream<String> getPaths(Class<?> clazz, String root, PathInits pathInits) {
if (EntityPath.class.isAssignableFrom(clazz) && pathInits.isInitialized(root)) {
return Arrays.stream(clazz.getFields())
.flatMap(
field ->
getPaths(
field.getType(),
appendPath(root, field.getName()),
pathInits.get(field.getName())));
} else if (Path.class.isAssignableFrom(clazz) && !ObjectUtils.isEmpty(root)) {
return Stream.of(root);
} else {
return Stream.of();
}
}
private String appendPath(String root, String path) {
if (Objects.equals(path, "_super")) {
return root;
} else if (ObjectUtils.isEmpty(root)) {
return path;
} else {
return String.format("%s.%s", root, path);
}
}
public void addParameter(Operation operation, Parameter... parameters) {
if (operation.getParameters() == null) {
operation.setParameters(new ArrayList<>());
}
operation.getParameters().addAll(Arrays.stream(parameters).toList());
}
Disclaimers:
This has undergone limited debugging and testing as of today, so use at your own risk.
This documents all initialized querydsl paths as string parameters. It would be cool to improve that using the actual schema type, but for my purposes this is good enough (since all query parameters have to become strings at some point anyway).
Actually doing this is very possibly a bad idea for many use cases, as many predicate options may incur very resource-intensive queries which could be abused. Use with caution and robust authorization controls.
As of this writing, Springdoc's integration with Spring Data REST has a significant performance problem, easily taking minutes to generate a spec for more than a few controllers and associations. This solution neither improves nor worsens that issue significantly. I'm just noting that here so that if others encounter it they are aware it is unrelated to this thread.
Versions that this worked with:
org.springframework.boot:spring-boot:3.4.1
org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6
com.querydsl:querydsl-core:5.1.0
com.querydsl:querydsl-jpa:5.1.0:jakarta