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 db2fba022f..654d89bc8e 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -61,15 +61,18 @@ import org.springframework.http.HttpStatus; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; -import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse; import org.thingsboard.server.exception.ThingsboardErrorResponse; import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.auth.rest.LoginResponse; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -303,15 +306,14 @@ public class SwaggerConfiguration { } } else if (schema != null && schema.getProperties() != null && !schema.getProperties().isEmpty()) { try { - var beanDesc = Json.mapper().getSerializationConfig().introspect(javaType); - var orderedNames = beanDesc.findProperties().stream() - .map(p -> p.getName()) - .toList(); + var beanDesc = JacksonUtil.OBJECT_MAPPER.getSerializationConfig().introspect(javaType); + var orderedNames = resolvePropertyOrder(cls, beanDesc); if (!orderedNames.isEmpty()) { + @SuppressWarnings("unchecked") Map current = schema.getProperties(); - var reordered = new LinkedHashMap(); + var reordered = new LinkedHashMap(); for (String name : orderedNames) { - Object prop = current.get(name); + Schema prop = current.get(name); if (prop != null) reordered.put(name, prop); } current.forEach((k, v) -> reordered.putIfAbsent(k, v)); @@ -674,6 +676,37 @@ public class SwaggerConfiguration { return new ApiResponse().description(description).content(content); } + private static List resolvePropertyOrder(Class cls, com.fasterxml.jackson.databind.BeanDescription beanDesc) { + // Map backing field names to their JSON property names (respects @JsonProperty) + Map fieldToJsonName = new LinkedHashMap<>(); + LinkedHashSet getterOnlyNames = new LinkedHashSet<>(); + for (var prop : beanDesc.findProperties()) { + if (prop.getField() != null) { + fieldToJsonName.put(prop.getField().getName(), prop.getName()); + } else { + getterOnlyNames.add(prop.getName()); + } + } + + // Walk class hierarchy (superclass first) to get field declaration order + List> hierarchy = new ArrayList<>(); + for (Class c = cls; c != null && c != Object.class; c = c.getSuperclass()) { + hierarchy.add(0, c); + } + List ordered = new ArrayList<>(); + for (Class c : hierarchy) { + for (Field f : c.getDeclaredFields()) { + if (Modifier.isStatic(f.getModifiers())) continue; + String jsonName = fieldToJsonName.get(f.getName()); + if (jsonName != null) ordered.add(jsonName); + } + } + + // Append getter-only properties (no backing field) at the end + ordered.addAll(getterOnlyNames); + return ordered; + } + private static Example errorExample(String summary, ThingsboardErrorResponse example) { var node = (ObjectNode) JacksonUtil.valueToTree(example); node.put("timestamp", 1609459200000L);