@ -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<EntityExportData<?>>>). 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 < Schema > allOf = schema . getAllOf ( ) ;
List < Schema > 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 ) ;