diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index 4e8e3721d3..072fe61252 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -62,7 +62,10 @@ import org.springframework.context.annotation.Profile; import org.springframework.http.HttpStatus; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.ContactBased; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse; import org.thingsboard.server.exception.ThingsboardErrorResponse; import org.thingsboard.server.service.security.auth.rest.LoginRequest; @@ -355,7 +358,17 @@ public class SwaggerConfiguration { .addSchemas("LoginResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(LoginResponse.class)).schema) .addSchemas("ThingsboardErrorResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardErrorResponse.class)).schema) .addSchemas("ThingsboardCredentialsExpiredResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardCredentialsExpiredResponse.class)).schema) - .addSchemas("ThingsboardErrorCode", errorCodeSchema); + .addSchemas("ThingsboardErrorCode", errorCodeSchema) + // Pre-register types to prevent springdoc resolution-order issues: + // - Types referenced with @JsonIgnoreProperties on fields (e.g. CalculatedField + // via EntityExportData.calculatedFields) to prevent field-level ignore lists + // from polluting the global schema. + // - Intermediate interfaces/classes (e.g. ContactBased, HasId) that springdoc + // only creates as "*Object" byproducts; pre-registering the base name lets + // the duplicate removal pass clean them up. + .addSchemas("CalculatedField", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(CalculatedField.class)).schema) + .addSchemas("ContactBased", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ContactBased.class)).schema) + .addSchemas("HasId", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(HasId.class)).schema); } private OperationCustomizer operationCustomizer() { @@ -407,18 +420,14 @@ public class SwaggerConfiguration { } }); - // Springdoc creates duplicate schemas with an "Object" suffix when a discriminated - // type (e.g. EntityExportData) is resolved through multiple paths: once via - // @Schema(implementation=...) and once via generic type resolution (e.g. from - // Map<..., List>>). The duplicate breaks allOf inheritance - // in generated clients. Remove it only when both schemas are structurally equal. + // Springdoc creates duplicate schemas with an "Object" suffix when a type is + // resolved through multiple inheritance paths or via generic type resolution. + // Remove the "*Object" duplicate when the base schema exists (either + // pre-registered in addDefaultSchemas or generated by springdoc). for (String name : new ArrayList<>(schemas.keySet())) { if (!name.endsWith("Object")) continue; String baseName = name.substring(0, name.length() - "Object".length()); - Schema baseSchema = schemas.get(baseName); - if (baseSchema == null) continue; - Schema objectSchema = schemas.get(name); - if (!baseSchema.equals(objectSchema)) continue; + if (!schemas.containsKey(baseName)) continue; schemas.remove(name); String refToRemove = "#/components/schemas/" + name; @@ -427,9 +436,28 @@ public class SwaggerConfiguration { s.getAllOf().removeIf(allOfEntry -> refToRemove.equals(((Schema) allOfEntry).get$ref())); } }); - log.debug("Removed duplicate schema '{}' (identical to '{}')", name, baseName); + log.debug("Removed duplicate schema '{}' (base '{}' exists)", name, baseName); } + // Remove duplicate inline entries in allOf (springdoc can generate identical + // property blocks when resolving a type through multiple parent paths). + schemas.values().forEach(schema -> { + if (schema.getAllOf() != null && schema.getAllOf().size() > 1) { + List allOf = schema.getAllOf(); + List deduplicated = new ArrayList<>(); + for (Schema entry : allOf) { + if (deduplicated.stream().noneMatch(entry::equals)) { + deduplicated.add(entry); + } + } + if (deduplicated.size() < allOf.size()) { + allOf.clear(); + allOf.addAll(deduplicated); + } + } + }); + + // Fix polymorphic properties: replace inline oneOf with base type $ref schemas.values().forEach(schema -> { replaceInlineOneOfProperties(schema, schemas);