Skip to content

When flatten = true, Jackson2HashMapper serializes the List and deserializes it out of order #2565

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ainydexiaohai opened this issue Apr 21, 2023 · 2 comments
Assignees
Labels
type: bug A general bug

Comments

@ainydexiaohai
Copy link

ainydexiaohai commented Apr 21, 2023

Hi,
When I tried to serialize an object containing the List attribute, I deserialized it to obtain a disordered List.

1.Problem

Test.class

Jackson2HashMapper jackson2HashMapper = new Jackson2HashMapper(true);
List<Integer> list = IntStream.range(0, 20).boxed().collect(Collectors.toList());
User user = new User();
user.setUserAgeList(list);
System.out.println("before serialize, user=" + user);
Map<String, Object> map = jackson2HashMapper.toHash(user);
User user1 = (User)jackson2HashMapper.fromHash(map);
System.out.println("after serialize, user=" + user1);

User.class

@Data
public class User {
    private List<Integer> userAgeList;
}

result:

before serialize, user=User(userAgeList=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
after serialize, user=User(userAgeList=[0, 1, 18, 3, 4, 2, 19, 7, 8, 5, 6, 9, 12, 13, 10, 11, 14, 15, 16, 17])

2.Reason

org.springframework.data.redis.hash.Jackson2HashMapper#flattenMap use HashMap to store elements.

private Map<String, Object> flattenMap(Iterator<Entry<String, JsonNode>> source) {
	Map<String, Object> resultMap = new HashMap();
	this.doFlatten("", source, resultMap);
	return resultMap;
}

3.Solution

Modify the storage set to LinkedHashMap.

private Map<String, Object> flattenMap(Iterator<Entry<String, JsonNode>> source) {
	Map<String, Object> resultMap = new LinkedHashMap();
	this.doFlatten("", source, resultMap);
	return resultMap;
}

Thanks in advance!

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Apr 21, 2023
@mp911de mp911de added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged labels Apr 25, 2023
@jxblum
Copy link
Contributor

jxblum commented May 7, 2023

@ainydexiaohai - Did you actually test your solution(?), because when I tried it, it did NOT work, and upon reviewing the applicable code, the resultMap in the Jackson2HashMapper.flattenMap(..) method is not what needs to be ordered in this case.

Please do not post solutions 1) especially, if you have not tested the solution (don't guess), 2) you are not reasonably confident you are correct in this case, and 3) see #1.

I did uncover the underlying problem and have already crafted a fix, which will appear in the next 2.7.x (2.7.12 service release and the final 2.7.x release), 3.0.x (3.0.6 service release) and the newly minted 3.1 GA release, all occuring on Friday, May 12th, 2023 (see Spring release calendar).

@jxblum jxblum added this to the 2.7.12 (2021.2.12) milestone May 7, 2023
@jxblum jxblum closed this as completed in a147385 May 7, 2023
jxblum added a commit to jxblum/spring-data-redis that referenced this issue May 7, 2023
jxblum added a commit to jxblum/spring-data-redis that referenced this issue May 7, 2023
jxblum added a commit to jxblum/spring-data-redis that referenced this issue May 7, 2023
@ainydexiaohai
Copy link
Author

@jxblum Without much understanding, why didn't it work? After I rewrote flattenMap, the order after serialization and deserialization remained the same.

1、overwrite

public class MyJackson2HashMapper extends Jackson2HashMapper {
    public MyJackson2HashMapper(boolean flatten) {
        super(flatten);
    }

    @Override
    public Map<String, Object> toHash(Object source) {
        boolean flatten = false;
        ObjectMapper typingMapper = null;
        ObjectMapper untypedMapper = null;
        try {
            Field flattenField = getClass().getSuperclass().getDeclaredField("flatten");
            flattenField.setAccessible(true);
            flatten = (Boolean) flattenField.get(this);

            Field typingMapperField = getClass().getSuperclass().getDeclaredField("typingMapper");
            typingMapperField.setAccessible(true);
            typingMapper = (ObjectMapper) typingMapperField.get(this);

            Field untypedMapperField = getClass().getSuperclass().getDeclaredField("untypedMapper");
            untypedMapperField.setAccessible(true);
            untypedMapper = (ObjectMapper) untypedMapperField.get(this);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }

        JsonNode tree = typingMapper.valueToTree(source);
        return flatten ? flattenMap(tree.fields()) : untypedMapper.convertValue(tree, Map.class);
    }

    private Map<String, Object> flattenMap(Iterator<Map.Entry<String, JsonNode>> source) {
        // HashMap -> LinkedHashMap
        Map<String, Object> resultMap = new LinkedHashMap<>();
        try {
            Method doFlattenMethod = getClass().getSuperclass().getDeclaredMethod("doFlatten", String.class, Iterator.class, Map.class);
            doFlattenMethod.setAccessible(true);
            doFlattenMethod.invoke(this, "", source, resultMap);
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return resultMap;
    }
}

2、Test

    @Test
    public void test1() {
        Jackson2HashMapper jackson2HashMapper = new Jackson2HashMapper(true);
        List<Integer> list = IntStream.range(0, 20).boxed().collect(Collectors.toList());
        User user = new User();
        user.setUserAgeList(list);
       // before serialize, user=User(userAgeList=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
        System.out.println("before serialize, user=" + user);
        Map<String, Object> map = jackson2HashMapper.toHash(user);
        User user1 = (User)jackson2HashMapper.fromHash(map);
        // after serialize, user=User(userAgeList=[0, 1, 18, 3, 4, 2, 19, 7, 8, 5, 6, 9, 12, 13, 10, 11, 14, 15, 16, 17])
        System.out.println("after serialize, user=" + user1);
    }

    @Test
    public void test2() {
        MyJackson2HashMapper jackson2HashMapper = new MyJackson2HashMapper(true);
        List<Integer> list = IntStream.range(0, 20).boxed().collect(Collectors.toList());
        User user = new User();
        user.setUserAgeList(list);
       // before serialize, user=User(userAgeList=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
        System.out.println("before serialize, user=" + user);
        Map<String, Object> map = jackson2HashMapper.toHash(user);
        User user1 = (User)jackson2HashMapper.fromHash(map);
        // after serialize, user=User(userAgeList=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
        System.out.println("after serialize, user=" + user1);
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug A general bug
Projects
None yet
Development

No branches or pull requests

4 participants