79603817

Date: 2025-05-02 17:13:21
Score: 0.5
Natty:
Report link

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.

Does Springdoc support this?

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.

How should this be supported, ideally?

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.

So how about a hacky solution for today?

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:

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
Reasons:
  • Blacklisted phrase (1): This document
  • Whitelisted phrase (-1): it worked
  • Long answer (-1):
  • Has code block (-0.5):
  • Contains question mark (0.5):
  • Self-answer (0.5):
  • Low reputation (1):
Posted by: Ben