springfox: Custom sorting/position is not honored for V2

Using an annotation such as:

@ApiOperation(
         value = "Operation",
         notes = "Notes",
         position = 2
 )

and a custom ordering such as:

docket.apiDescriptionOrdering(new Ordering<ApiDescription>() {

        @Override
        public int compare(ApiDescription left, ApiDescription right) {
            int leftPos = left.getOperations().size() == 1 ? left.getOperations().get(0).getPosition() : 0;
            int rightPos = right.getOperations().size() == 1 ? right.getOperations().get(0).getPosition() : 0;

            int position = Integer.compare(leftPos, rightPos);

            if(position == 0) {
                position = left.getPath().compareTo(right.getPath());
            }

            return position;
        }
    });

The scanner framework seems to read the documentation and sort it properly. When the data is extracted to the Swagger object at Swagger2Controller.java:74, the ordering seems to be lost.

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 26 (6 by maintainers)

Commits related to this issue

Most upvoted comments

So let me get this right… If your consumer has to follow a hateoas pattern: ie: Call1 must get a list of objects and returns Id to make call 2 Call2 must use id from Call1 to fetch an object that has a list of objects to fetch for Call3 by Id Call3 must use id from Call2 to retrieve last object

But somewhere, someone decided that Alphabetical was a good solution??? Umm… WHY? Now, I have to explain to all users of my swagger doc that they must start at 2, then goto 3 then goto 1 depending on how my endpoint is alphabetically arranged.

Pure and utter NONSENSE! FIX THIS!

I try new version 2.9.2,operationOrdering and apiListingReferenceOrdering not working!It’s a bug!

A LinkedHashMap preserves insertion order when iterated. If you insert things in sorted order, they will stay that way when retrieved via iteration.

It looks like the offending code is here. It’s certainly something that could be fixed by subclassing the Swagger object and getting rid of the extra sort.

class SortedSwagger extends Swagger {
    @Override
    public Map<String, Path> getPaths() {
        return paths;
    }
}

I’ve opened this issue at swagger-core.

I also encountered the same problem. And found an extended solution, I hope to help you, and may submit a PR later. Version 2.7.0 is work.

There are five steps here.

The first step is to add position where needed. For example:

/*
 *  Copyright 2019 the original author or authors.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Yahuan Jin
 * @since 2019.08.24
 */
@Api(tags = "test", description = "test")
@RequestMapping("test/apiOperation")
@RestController
public class ApiOperationTestController {
    @ApiOperation(value = "Test position 2", tags = {"position-test"}, position = 2)
    @PostMapping(value = "aaa")
    public String testPositionAaa() {
        return "aaa";
    }

    @ApiOperation(value = "Test position 1", tags = {"position-test"}, position = 1)
    @RequestMapping(value = "bbb")
    public String testPositionBbb() {
        return "bbb";
    }
}

The second step is to support the position field, rewriting springfox.documentation.spring.web.readers.operation.ApiOperationReader. For example:

/*
 *  Copyright 2015-2019 the original author or authors.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

import com.google.common.base.Optional;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import springfox.documentation.OperationNameGenerator;
import springfox.documentation.builders.OperationBuilder;
import springfox.documentation.service.Operation;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.spi.service.contexts.RequestMappingContext;
import springfox.documentation.spring.web.plugins.DocumentationPluginsManager;
import springfox.documentation.spring.web.readers.operation.OperationReader;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import static com.google.common.collect.Lists.newArrayList;
import static java.util.Arrays.asList;

/**
 * @author {@link springfox.documentation.spring.web.readers.operation.ApiOperationReader
 * the original author}
 * @author Yahuan Jin
 * @see springfox.documentation.spring.web.readers.operation.ApiOperationReader
 * @since 2019.08.24
 */
@Component
public class SwaggerSupportPositionApiOperationReader implements OperationReader {

    private static final Set<RequestMethod> allRequestMethods
            = new LinkedHashSet<>(asList(RequestMethod.values()));
    private final DocumentationPluginsManager pluginsManager;
    private final OperationNameGenerator nameGenerator;

    @Autowired
    public SwaggerSupportPositionApiOperationReader(DocumentationPluginsManager pluginsManager,
                                                    OperationNameGenerator nameGenerator) {
        this.pluginsManager = pluginsManager;
        this.nameGenerator = nameGenerator;
    }

    @Override
    public List<Operation> read(RequestMappingContext outerContext) {
        List<Operation> operations = newArrayList();

        Set<RequestMethod> requestMethods = outerContext.getMethodsCondition();
        Set<RequestMethod> supportedMethods = supportedMethods(requestMethods);

        //Setup response message list
        Integer currentCount = 0;
        // Get position, then support position. NOTE: not support sorted by RequestMethod.
        int position = getApiOperationPosition(outerContext, 0);

        for (RequestMethod httpRequestMethod : supportedMethods) {
            OperationContext operationContext = new OperationContext(
                    new OperationBuilder(nameGenerator),
                    httpRequestMethod,
                    outerContext,
                    (position + currentCount));

            Operation operation = pluginsManager.operation(operationContext);
            if (!operation.isHidden()) {
                operations.add(operation);
                currentCount++;
            }
        }
        Collections.sort(operations, outerContext.operationOrdering());

        return operations;
    }

    private Set<RequestMethod> supportedMethods(final Set<RequestMethod> requestMethods) {
        return requestMethods == null || requestMethods.isEmpty()
                ? allRequestMethods
                : requestMethods;
    }

    private int getApiOperationPosition(final RequestMappingContext outerContext, final int defaultValue) {
        final Optional<ApiOperation> annotation = outerContext.findAnnotation(ApiOperation.class);
        return annotation.isPresent() ? annotation.get().position() : defaultValue;
    }
}

The third step is to not reset the sort of api list, rewriting springfox.documentation.swagger2.mappers.ServiceModelToSwagger2MapperImpl. For example:

/*
 *  Copyright 2015-2019 the original author or authors.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

import com.google.common.base.Optional;
import com.google.common.collect.Multimap;
import io.swagger.models.Operation;
import io.swagger.models.Path;
import io.swagger.models.Swagger;
import org.springframework.stereotype.Component;
import springfox.documentation.service.ApiDescription;
import springfox.documentation.service.ApiListing;
import springfox.documentation.service.Documentation;
import springfox.documentation.swagger2.mappers.ServiceModelToSwagger2MapperImpl;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

import static springfox.documentation.builders.BuilderDefaults.nullToEmptyList;

/**
 * @author {@link springfox.documentation.swagger2.mappers.ServiceModelToSwagger2MapperImpl
 * the original author}
 * @author Yahuan Jin
 * @see springfox.documentation.swagger2.mappers.ServiceModelToSwagger2MapperImpl
 * @since 2019.08.24
 */
@Component
public class SwaggerOriginalSortedServiceModelToSwagger2MapperImpl extends ServiceModelToSwagger2MapperImpl {

    @Override
    public Swagger mapDocumentation(Documentation from) {
        final Swagger swagger = super.mapDocumentation(from);

        if (Objects.isNull(swagger)) {
            return null;
        }

        Map<String, Path> map__ = mapOriginalSortedApiListings(from.getApiListings());
        if (map__ != null) {
            swagger.setPaths(map__);
        }

        return swagger;
    }

    private Map<String, Path> mapOriginalSortedApiListings(Multimap<String, ApiListing> apiListings) {
        Map<String, Path> paths = new LinkedHashMap<>();
        for (ApiListing each : apiListings.values()) {
            for (ApiDescription api : each.getApis()) {
                paths.put(api.getPath(), mapOperations(api, Optional.fromNullable(paths.get(api.getPath()))));
            }
        }
        return paths;
    }

    private Path mapOperations(ApiDescription api, Optional<Path> existingPath) {
        Path path = existingPath.or(new Path());
        for (springfox.documentation.service.Operation each : nullToEmptyList(api.getOperations())) {
            Operation operation = mapOperation(each);
            path.set(each.getMethod().toString().toLowerCase(), operation);
        }
        return path;
    }
}

The fourth step is to override the default two beans. The last step, specify the collation. For example:

/*
 *  Copyright 2019 the original author or authors.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

import com.google.common.collect.Ordering;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import springfox.documentation.OperationNameGenerator;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiDescription;
import springfox.documentation.service.Operation;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.DocumentationPluginsManager;
import springfox.documentation.spring.web.readers.operation.OperationReader;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import springfox.documentation.swagger2.mappers.ServiceModelToSwagger2Mapper;

import java.util.List;
/**
 * @author Yahuan Jin
 * @since 2019.08.24
 */
@EnableSwagger2
@Configuration
public class Swagger2Config {

    @Bean
    public Docket v1ApiDocket() {
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com"))
                .paths(PathSelectors.any())
                .build();

        // Support position
        docket.apiDescriptionOrdering(new Ordering<ApiDescription>() {
            @Override
            public int compare(ApiDescription a, ApiDescription b) {
                final List<Operation> leftList = a.getOperations();
                final List<Operation> rightList = b.getOperations();
                return Integer.compare(leftList.get(0).getPosition(), rightList.get(0).getPosition());
            }
        });

        return docket;
    }

    @Primary
    @Bean(name = "default")
    public OperationReader operationReader(
            DocumentationPluginsManager pluginsManager, OperationNameGenerator nameGenerator) {
        return new SwaggerSupportPositionApiOperationReader(pluginsManager, nameGenerator);
    }

    @Primary
    @Bean(name = "serviceModelToSwagger2Mapper")
    public ServiceModelToSwagger2Mapper SwaggerOriginalSortedServiceModelToSwagger2MapperImpl() {
        return new SwaggerOriginalSortedServiceModelToSwagger2MapperImpl();
    }
}

Then see api page!

Back to original post. The position field is deprecated in latest release and is not supported. This very important feature is required for a simple way to order endpoints in a controller with default tags.

@ApiOperation(
         value = "Operation",
         notes = "Notes",
         position = 2
)

it make me just can do this for sorting @Api(value = "xxx", tags = "1", description = "hello1") @Api(value = "xxx", tags = "2", description = "hello2") @Api(value = "xxx", tags = "3", description = "hello3")

@jinyahuan Thanks for the hint! However, to be honest, I don’t like this bootstrap UI and I’d prefer using springfox-swagger-ui.

Personally, I think this field should not be deprecated nor removed from springfox-swagger-ui - this is embarrassing…

Pure and utter NONSENSE! FIX THIS!

@grtessman if you want an open source project fixed contribute to the code yourself. Don’t be so rude to those who maintain it.

Sounds like some BS, removed important feature with no care to fix.