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 4e2f58e864..6030f95c65 100644
--- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
@@ -446,11 +446,10 @@ public class SwaggerConfiguration {
});
// Deduplicate allOf child schemas: remove properties that are already defined
- // in the referenced parent schema to avoid duplication (e.g. EntityId children)
- schemas.values().forEach(schema -> deduplicateAllOfProperties(schema, schemas));
-
- // Clean up internal marker extension used by deduplicateAllOfProperties
+ // in the referenced parent schema to avoid duplication (e.g. EntityId children),
+ // then clean up the internal marker extension used during deduplication.
schemas.values().forEach(schema -> {
+ deduplicateAllOfProperties(schema, schemas);
if (schema.getExtensions() != null) {
schema.getExtensions().remove("x-tb-own-props");
if (schema.getExtensions().isEmpty()) {
@@ -845,6 +844,26 @@ public class SwaggerConfiguration {
return own;
}
+ /**
+ * Resolves the property ordering for a schema class.
+ *
+ *
Returns a list of JSON property names in the order they should appear in the
+ * OpenAPI schema. The caller uses this list to reorder the schema's property map;
+ * any properties not present in the returned list are appended alphabetically
+ * by the caller's {@code TreeMap} fallback, guaranteeing a stable, deterministic order.
+ *
+ *
Resolution strategy (first match wins):
+ *
+ * - If {@code @JsonPropertyOrder} with an explicit {@code value()} is found on the
+ * class or any interface in its ancestry, that list is returned as-is. Note: if the
+ * annotation lists only a subset of fields, those fields are ordered first and the
+ * remaining properties fall through to the caller's alphabetical fallback — consistent
+ * with Jackson's own behaviour for partial {@code @JsonPropertyOrder}.
+ * - Otherwise, field-backed properties are returned in declaration order (superclass
+ * fields first). Getter-only properties are intentionally excluded to avoid
+ * non-deterministic ordering across restarts.
+ *
+ */
private static List resolvePropertyOrder(Class> cls, com.fasterxml.jackson.databind.BeanDescription beanDesc) {
// If an explicit @JsonPropertyOrder is present on the class or any interface in its
// ancestry, honour it directly. Walk up the class hierarchy; for each class also walk
diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java
index d645d40dcc..9d6434a7c2 100644
--- a/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java
+++ b/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java
@@ -15,8 +15,18 @@
*/
package org.thingsboard.server.common.data.id;
+import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
+import io.swagger.v3.oas.annotations.media.Schema;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
public class EntityIdTest {
@@ -25,4 +35,44 @@ public class EntityIdTest {
Assertions.assertEquals("13814000-1dd2-11b2-8080-808080808080", EntityId.NULL_UUID.toString());
}
+ @Test
+ public void allEntityIdImplementors_shouldBeInDiscriminatorMapping() {
+ Schema schemaAnnotation = EntityId.class.getAnnotation(Schema.class);
+ assertThat(schemaAnnotation).as("EntityId must have @Schema annotation").isNotNull();
+
+ DiscriminatorMapping[] mappings = schemaAnnotation.discriminatorMapping();
+ Map> discriminatorMap = Arrays.stream(mappings)
+ .collect(Collectors.toMap(DiscriminatorMapping::value, DiscriminatorMapping::schema));
+
+ UUID testUuid = UUID.randomUUID();
+ for (EntityType entityType : EntityType.values()) {
+ EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, testUuid);
+ String typeName = entityType.name();
+
+ assertThat(discriminatorMap)
+ .as("EntityId @Schema discriminatorMapping is missing entry for EntityType." + typeName)
+ .containsKey(typeName);
+ assertThat(discriminatorMap.get(typeName))
+ .as("Discriminator mapping for " + typeName + " should point to " + entityId.getClass().getSimpleName())
+ .isEqualTo(entityId.getClass());
+ }
+ }
+
+ @Test
+ public void allEntityIdImplementors_shouldHaveAllOfEntityId() {
+ UUID testUuid = UUID.randomUUID();
+ for (EntityType entityType : EntityType.values()) {
+ EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, testUuid);
+ Class> idClass = entityId.getClass();
+ Schema schemaAnnotation = idClass.getAnnotation(Schema.class);
+
+ assertThat(schemaAnnotation)
+ .as(idClass.getSimpleName() + " must have @Schema annotation")
+ .isNotNull();
+ assertThat(schemaAnnotation.allOf())
+ .as(idClass.getSimpleName() + " @Schema must include allOf = EntityId.class")
+ .contains(EntityId.class);
+ }
+ }
+
}
\ No newline at end of file