Search code examples
javaswaggerswagger-ui

Can we somehow give Swagger UI documentation JSONs directly?


I have a Gateway service with dynamically created routes. I want it to expose its endpoints through Swagger UI. Since my routes are not static (they come and go as new services are registered and deregistered with Eureka), I can't do it purely with static yamls (like so). So what I did was:

  1. specifying a configuration endpoint;
springdoc:
  api-docs:
    enabled: false
  swagger-ui:
    enabled: true
    path: /swagger-ui.html
    config-url: /swagger-ui-config
  1. providing support for that endpoint;
package by.afinny.apigateway.controller;

import by.afinny.apigateway.model.uiConfig.SwaggerUiConfig;
import by.afinny.apigateway.service.SwaggerUiConfigProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
@RequiredArgsConstructor
public class SwaggerUiConfigController {
    private final SwaggerUiConfigProvider configProvider;
    @GetMapping("/swagger-ui-config")
    public Mono<SwaggerUiConfig> getConfig() {
        return configProvider.getSwaggerUiConfig();
    }
}
  1. creating classes that encapsulate configuration Swagger UI expects (a list of url-name pairs);
package by.afinny.apigateway.model.uiConfig;

import by.afinny.apigateway.model.documentedApplication.SwaggerApplication;
import by.afinny.apigateway.service.SwaggerUiConfigSerializer;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.Collection;

@NoArgsConstructor
@Getter
public class SwaggerUiConfig {
    @JsonProperty("urls")
    @JsonSerialize(contentUsing = SwaggerUiConfigSerializer.class)
    private Collection<SwaggerApplication> swaggerApplications;

    public SwaggerUiConfig(Collection<SwaggerApplication> swaggerApplications) {
        this.swaggerApplications = swaggerApplications;
    }

    public static SwaggerUiConfig from(Collection<SwaggerApplication> swaggerApplications) {
        return new SwaggerUiConfig(swaggerApplications);
    }
}
package by.afinny.apigateway.service;

import by.afinny.apigateway.model.documentedApplication.SwaggerApplication;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import lombok.SneakyThrows;

import java.io.IOException;
import java.text.MessageFormat;

public class SwaggerUiConfigSerializer extends JsonSerializer<SwaggerApplication> {
    @Override
    @SneakyThrows
    public void serialize(SwaggerApplication swaggerApplication, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeStartObject();
        jsonGenerator.writeStringField("url", MessageFormat.format("/{0}{1}", swaggerApplication.getName(), SwaggerApplication.SWAGGER_DOC_PATH));
        jsonGenerator.writeStringField("name", swaggerApplication.getName());
        jsonGenerator.writeEndObject();
    }
}
  1. providing routes corresponding to those urls; the routes simply forward to lb://SERVICE-NAME/v3/api-docs
// I'll omit that code

I won't provide all the code since it doesn't really matter, and I don't want to overwhelm you. I wanted to show you a general flow

But here's the thing: I already fetched those JSON docs to build my business routes (a route per endpoint), they are already stored in memory (and updated). I don't want to go through all that fuss just to let Swagger do that unnecessary extra work of fetching the docs again

Can I somehow give Swagger UI docs directly instead of telling it where to find them?


Solution

  • The most direct way I could find is writing endpoints for both the config and the docs themselves. Doc endpoints may return cached OpenAPI objects

    package com.example.dynamicgateway.controller;
    
    import com.example.dynamicgateway.model.uiConfig.SwaggerUiConfig;
    import com.example.dynamicgateway.service.swaggerUiSupport.SwaggerUiSupport;
    import lombok.RequiredArgsConstructor;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    import reactor.core.publisher.Mono;
    
    @RestController
    @RequiredArgsConstructor
    public class SwaggerUiConfigController {
        private final SwaggerUiSupport uiSupport;
    
        @GetMapping("/swagger-ui-config")
        public Mono<SwaggerUiConfig> getConfig() {
            return uiSupport.getSwaggerUiConfig();
        }
    }
    
    package com.example.dynamicgateway.controller;
    
    import com.example.dynamicgateway.service.swaggerUiSupport.SwaggerUiSupport;
    import io.swagger.v3.oas.models.OpenAPI;
    import lombok.RequiredArgsConstructor;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RestController;
    import reactor.core.publisher.Mono;
    
    @RestController
    @RequiredArgsConstructor
    public class SwaggerDocController {
        private final SwaggerUiSupport uiSupport;
    
        @GetMapping("{application-name}/doc")
        public Mono<OpenAPI> getSwaggerAppDoc(@PathVariable("application-name") String applicationName) {
            // return cached OpenAPI object right away
            return uiSupport.getSwaggerAppDoc(applicationName);
        }
    }
    
    package com.example.dynamicgateway.model.uiConfig;
    
    import com.example.dynamicgateway.model.documentedApplication.SwaggerApplication;
    import com.example.dynamicgateway.service.swaggerUiSupport.SwaggerUiConfigSerializer;
    import com.fasterxml.jackson.annotation.JsonProperty;
    import com.fasterxml.jackson.databind.annotation.JsonSerialize;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    
    import java.util.Collection;
    
    @NoArgsConstructor
    @Getter
    public class SwaggerUiConfig {
        @JsonProperty("urls")
        @JsonSerialize(contentUsing = SwaggerUiConfigSerializer.class)
        private Collection<SwaggerApplication> swaggerApplications;
    
        public SwaggerUiConfig(Collection<SwaggerApplication> swaggerApplications) {
            this.swaggerApplications = swaggerApplications;
        }
    
        public static SwaggerUiConfig from(Collection<SwaggerApplication> swaggerApplications) {
            return new SwaggerUiConfig(swaggerApplications);
        }
    }
    
    package com.example.dynamicgateway.service.swaggerUiSupport;
    
    import com.example.dynamicgateway.model.documentedApplication.SwaggerApplication;
    import com.fasterxml.jackson.core.JsonGenerator;
    import com.fasterxml.jackson.databind.JsonSerializer;
    import com.fasterxml.jackson.databind.SerializerProvider;
    import lombok.SneakyThrows;
    
    import java.text.MessageFormat;
    
    public class SwaggerUiConfigSerializer extends JsonSerializer<SwaggerApplication> {
        @Override
        @SneakyThrows
        public void serialize(SwaggerApplication swaggerApplication, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) {
            jsonGenerator.writeStartObject();
            jsonGenerator.writeStringField("url", MessageFormat.format("/{0}/doc", swaggerApplication.getName()));
            jsonGenerator.writeStringField("name", swaggerApplication.getName());
            jsonGenerator.writeEndObject();
        }
    }
    
    springdoc:
      swagger-ui:
        enabled: true
        path: /swagger-ui.html
        config-url: /swagger-ui-config