Created
March 22, 2026 13:19
-
-
Save saillinux/6a02d68e75cc4da28c453ee8ecfb6f56 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.schooldevops.apifirst.apifirstsamples.config; | |
| import io.swagger.annotations.ApiModelProperty; | |
| import io.swagger.annotations.Extension; | |
| import io.swagger.annotations.ExtensionProperty; | |
| import io.swagger.v3.oas.models.OpenAPI; | |
| import io.swagger.v3.oas.models.media.Schema; | |
| import lombok.extern.slf4j.Slf4j; | |
| import org.springframework.core.Ordered; | |
| import org.springframework.core.annotation.Order; | |
| import org.springframework.stereotype.Component; | |
| import springfox.documentation.oas.web.OpenApiTransformationContext; | |
| import springfox.documentation.oas.web.WebMvcOpenApiTransformationFilter; | |
| import springfox.documentation.spi.DocumentationType; | |
| import javax.servlet.http.HttpServletRequest; | |
| import java.lang.reflect.Field; | |
| import java.util.LinkedHashMap; | |
| import java.util.Map; | |
| /** | |
| * SpringFox 3.0.0 does not serialize {@code @ApiModelProperty} extensions into the | |
| * generated OpenAPI 3.0 schema properties. This filter post-processes the spec | |
| * and injects them by reflecting on the registered DTO classes. | |
| */ | |
| @Slf4j | |
| @Component | |
| @Order(Ordered.LOWEST_PRECEDENCE) | |
| public class ModelPropertyExtensionFilter implements WebMvcOpenApiTransformationFilter { | |
| /** | |
| * Map of OpenAPI schema title -> Java class. | |
| * Register every DTO that uses @ApiModelProperty(extensions = ...) here. | |
| */ | |
| private static final Map<String, Class<?>> SCHEMA_CLASS_MAP = new LinkedHashMap<>(); | |
| static { | |
| SCHEMA_CLASS_MAP.put("Product", | |
| com.schooldevops.apifirst.apifirstsamples.dto.ProductDto.class); | |
| SCHEMA_CLASS_MAP.put("ErrorResponse", | |
| com.schooldevops.apifirst.apifirstsamples.dto.ErrorResponseDto.class); | |
| } | |
| @Override | |
| public OpenAPI transform(OpenApiTransformationContext<HttpServletRequest> context) { | |
| OpenAPI openApi = context.getSpecification(); | |
| if (openApi.getComponents() == null || openApi.getComponents().getSchemas() == null) { | |
| return openApi; | |
| } | |
| Map<String, Schema> schemas = openApi.getComponents().getSchemas(); | |
| for (Map.Entry<String, Class<?>> entry : SCHEMA_CLASS_MAP.entrySet()) { | |
| String schemaName = entry.getKey(); | |
| Class<?> dtoClass = entry.getValue(); | |
| Schema<?> schema = schemas.get(schemaName); | |
| if (schema == null || schema.getProperties() == null) { | |
| continue; | |
| } | |
| Map<String, Schema> properties = schema.getProperties(); | |
| for (Field field : dtoClass.getDeclaredFields()) { | |
| ApiModelProperty annotation = field.getAnnotation(ApiModelProperty.class); | |
| if (annotation == null) { | |
| continue; | |
| } | |
| Extension[] extensions = annotation.extensions(); | |
| if (extensions.length == 0) { | |
| continue; | |
| } | |
| Schema<?> propertySchema = properties.get(field.getName()); | |
| if (propertySchema == null) { | |
| continue; | |
| } | |
| for (Extension ext : extensions) { | |
| String extName = ext.name(); | |
| // Skip default/empty extensions (annotation defaults) | |
| if (extName.isEmpty() && ext.properties().length == 0) { | |
| continue; | |
| } | |
| // Skip extensions with only empty properties | |
| boolean hasRealProperties = false; | |
| for (ExtensionProperty prop : ext.properties()) { | |
| if (!prop.name().isEmpty()) { | |
| hasRealProperties = true; | |
| break; | |
| } | |
| } | |
| if (!hasRealProperties) { | |
| continue; | |
| } | |
| if (!extName.startsWith("x-")) { | |
| extName = "x-" + extName; | |
| } | |
| Map<String, String> extValues = new LinkedHashMap<>(); | |
| for (ExtensionProperty prop : ext.properties()) { | |
| if (!prop.name().isEmpty()) { | |
| extValues.put(prop.name(), prop.value()); | |
| } | |
| } | |
| propertySchema.addExtension(extName, extValues); | |
| log.debug("Added extension '{}' to {}.{}", extName, schemaName, field.getName()); | |
| } | |
| } | |
| } | |
| return openApi; | |
| } | |
| @Override | |
| public boolean supports(DocumentationType delimiter) { | |
| return delimiter == DocumentationType.OAS_30; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.schooldevops.apifirst.apifirstsamples.config; | |
| import com.fasterxml.jackson.annotation.JsonAnyGetter; | |
| import com.fasterxml.jackson.annotation.JsonAutoDetect; | |
| import com.fasterxml.jackson.annotation.JsonIgnore; | |
| import com.fasterxml.jackson.annotation.JsonInclude; | |
| import com.fasterxml.jackson.databind.ObjectMapper; | |
| import io.swagger.v3.oas.models.media.Schema; | |
| import lombok.extern.slf4j.Slf4j; | |
| import org.springframework.beans.factory.SmartInitializingSingleton; | |
| import org.springframework.stereotype.Component; | |
| import springfox.documentation.spring.web.json.JsonSerializer; | |
| import java.lang.reflect.Field; | |
| import java.util.Map; | |
| /** | |
| * Fixes the serialization of vendor extensions on {@link Schema} properties. | |
| * | |
| * <p>SpringFox's {@code OpenApiJacksonModule} registers a plain {@code NonEmptyMixin} | |
| * for {@link Schema} that serializes extensions as a nested object: | |
| * {@code "extensions": {"x-foo": ...}}. | |
| * | |
| * <p>The OpenAPI 3.0 spec requires them as flattened top-level keys: | |
| * {@code "x-foo": ...}. | |
| * | |
| * <p>This component runs after all singletons are initialized, extracts the | |
| * {@code ObjectMapper} from SpringFox's {@link JsonSerializer}, and replaces | |
| * the {@link Schema} mixin with one that uses {@code @JsonAnyGetter}. | |
| */ | |
| @Slf4j | |
| @Component | |
| public class OpenApiExtensionSerializationConfig implements SmartInitializingSingleton { | |
| private final JsonSerializer jsonSerializer; | |
| public OpenApiExtensionSerializationConfig(JsonSerializer jsonSerializer) { | |
| this.jsonSerializer = jsonSerializer; | |
| } | |
| @Override | |
| public void afterSingletonsInstantiated() { | |
| try { | |
| Field mapperField = JsonSerializer.class.getDeclaredField("objectMapper"); | |
| mapperField.setAccessible(true); | |
| ObjectMapper mapper = (ObjectMapper) mapperField.get(jsonSerializer); | |
| mapper.addMixIn(Schema.class, SchemaExtensionMixin.class); | |
| log.info("Applied SchemaExtensionMixin to SpringFox's ObjectMapper — " + | |
| "vendor extensions will be serialized as top-level x-* keys"); | |
| } catch (NoSuchFieldException | IllegalAccessException e) { | |
| log.warn("Failed to patch SpringFox ObjectMapper for extension serialization: {}", e.getMessage()); | |
| } | |
| } | |
| /** | |
| * Mixin that flattens the extensions map into top-level {@code x-*} keys | |
| * and hides the internal {@code exampleSetFlag} property. | |
| */ | |
| @JsonAutoDetect | |
| @JsonInclude(JsonInclude.Include.NON_EMPTY) | |
| abstract static class SchemaExtensionMixin { | |
| @JsonIgnore | |
| abstract boolean getExampleSetFlag(); | |
| @JsonAnyGetter | |
| abstract Map<String, Object> getExtensions(); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment