Skip to content

Commit 0212bfb

Browse files
pmbrullclaude
authored andcommitted
Fix EntityReference deserialization error when listing entities with sortBy (#28616)
Listing entities with `sortBy` routes to the search-index path, which rebuilds each entity from the Elasticsearch `_source`. Search documents store `followers` as a flat list of UUID strings (keyword field), which cannot be deserialized into the entity's `List<EntityReference> followers`, producing a 400 on endpoints like `/contextCenter/pages?sortBy=updatedAt`. Normalize followers back into user EntityReferences before deserialization in EntityRepository.listFromSearchWithOffset. The read-side fix preserves the keyword-ID index format that the follow/unfollow painless scripts depend on, so no reindex is required. Applies to every entity that uses this search list path. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit cfa9238)
1 parent 6973051 commit 0212bfb

3 files changed

Lines changed: 68 additions & 0 deletions

File tree

openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@
244244
import org.openmetadata.service.resources.teams.RoleResource;
245245
import org.openmetadata.service.rules.RuleEngine;
246246
import org.openmetadata.service.search.PropagationDescriptor;
247+
import org.openmetadata.service.search.SearchIndexUtils;
247248
import org.openmetadata.service.search.SearchListFilter;
248249
import org.openmetadata.service.search.SearchRepository;
249250
import org.openmetadata.service.search.SearchResultListMapper;
@@ -4053,6 +4054,7 @@ public ResultList<T> listFromSearchWithOffset(
40534054
subjectContext);
40544055
total = results.getTotal();
40554056
for (Map<String, Object> json : results.getResults()) {
4057+
SearchIndexUtils.normalizeFollowers(json);
40564058
T entity = JsonUtils.readOrConvertValueLenient(json, entityClass);
40574059
entityList.add(withHref(uriInfo, entity));
40584060
}

openmetadata-service/src/main/java/org/openmetadata/service/search/SearchIndexUtils.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Map;
2020
import java.util.Optional;
2121
import java.util.Set;
22+
import java.util.UUID;
2223
import java.util.stream.Collectors;
2324
import lombok.Getter;
2425
import lombok.extern.slf4j.Slf4j;
@@ -36,6 +37,7 @@
3637
import org.openmetadata.schema.type.change.ChangeSource;
3738
import org.openmetadata.schema.type.change.ChangeSummary;
3839
import org.openmetadata.schema.utils.JsonUtils;
40+
import org.openmetadata.service.Entity;
3941
import org.openmetadata.service.TypeRegistry;
4042
import org.openmetadata.service.util.Utilities;
4143

@@ -166,6 +168,32 @@ public static List<String> parseFollowers(List<EntityReference> followersRef) {
166168
return followersRef.stream().map(item -> item.getId().toString()).toList();
167169
}
168170

171+
/**
172+
* Search documents store {@code followers} as a flat list of user-id strings (see {@link
173+
* #parseFollowers}). When a search hit is converted back into an entity, that string list cannot
174+
* be deserialized into the entity's {@code List<EntityReference> followers} field. This rebuilds
175+
* each id into a user {@link EntityReference} so the entity deserializes cleanly.
176+
*/
177+
public static void normalizeFollowers(Map<String, Object> sourceAsMap) {
178+
Object followers = sourceAsMap.get(EntityBuilderConstant.FIELD_FOLLOWERS);
179+
if (followers instanceof List<?> followerList && !followerList.isEmpty()) {
180+
sourceAsMap.put(EntityBuilderConstant.FIELD_FOLLOWERS, expandFollowerIds(followerList));
181+
}
182+
}
183+
184+
private static List<Object> expandFollowerIds(List<?> followerList) {
185+
List<Object> followers = new ArrayList<>();
186+
for (Object follower : followerList) {
187+
if (follower instanceof String followerId) {
188+
followers.add(
189+
new EntityReference().withId(UUID.fromString(followerId)).withType(Entity.USER));
190+
} else {
191+
followers.add(follower);
192+
}
193+
}
194+
return followers;
195+
}
196+
169197
public static List<String> parseOwners(List<EntityReference> ownersRef) {
170198
if (ownersRef == null) {
171199
return Collections.emptyList();

openmetadata-service/src/test/java/org/openmetadata/service/search/SearchIndexUtilsTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.UUID;
1414
import org.junit.jupiter.api.Test;
1515
import org.mockito.MockedStatic;
16+
import org.openmetadata.schema.entity.data.Page;
1617
import org.openmetadata.schema.entity.data.Table;
1718
import org.openmetadata.schema.entity.data.Topic;
1819
import org.openmetadata.schema.tests.DataQualityReport;
@@ -25,6 +26,8 @@
2526
import org.openmetadata.schema.type.TagLabel;
2627
import org.openmetadata.schema.type.change.ChangeSource;
2728
import org.openmetadata.schema.type.change.ChangeSummary;
29+
import org.openmetadata.schema.utils.JsonUtils;
30+
import org.openmetadata.service.Entity;
2831
import org.openmetadata.service.TypeRegistry;
2932

3033
class SearchIndexUtilsTest {
@@ -63,6 +66,41 @@ void testParseHelpersAndRemoveFieldByPathSupportsNestedLists() {
6366
assertFalse(doc.containsKey("simple"));
6467
}
6568

69+
@Test
70+
void testNormalizeFollowersExpandsIdStringsIntoEntityReferences() {
71+
UUID followerId = UUID.randomUUID();
72+
Map<String, Object> source = new HashMap<>();
73+
source.put("id", UUID.randomUUID().toString());
74+
source.put("name", "test-page");
75+
source.put("pageType", "Article");
76+
source.put("followers", new ArrayList<>(List.of(followerId.toString())));
77+
78+
assertThrows(
79+
IllegalArgumentException.class,
80+
() -> JsonUtils.readOrConvertValueLenient(source, Page.class));
81+
82+
SearchIndexUtils.normalizeFollowers(source);
83+
Page page = JsonUtils.readOrConvertValueLenient(source, Page.class);
84+
85+
assertEquals(1, page.getFollowers().size());
86+
EntityReference follower = page.getFollowers().getFirst();
87+
assertEquals(followerId, follower.getId());
88+
assertEquals(Entity.USER, follower.getType());
89+
}
90+
91+
@Test
92+
void testNormalizeFollowersLeavesEmptyOrMissingFollowersUntouched() {
93+
Map<String, Object> missing = new HashMap<>();
94+
missing.put("name", "no-followers");
95+
SearchIndexUtils.normalizeFollowers(missing);
96+
assertFalse(missing.containsKey("followers"));
97+
98+
Map<String, Object> empty = new HashMap<>();
99+
empty.put("followers", new ArrayList<>());
100+
SearchIndexUtils.normalizeFollowers(empty);
101+
assertEquals(List.of(), empty.get("followers"));
102+
}
103+
66104
@Test
67105
void testBuildAggregationTreeAndParseMetricAggregationReport() {
68106
SearchAggregation aggregation =

0 commit comments

Comments
 (0)