FasterXML / jackson-databind

General data-binding package for Jackson (2.x): works on streaming API (core) implementation(s)
Apache License 2.0
3.52k stars 1.38k forks source link

Ability to specify array (de)serialization order of sets #3166

Open mjustin opened 3 years ago

mjustin commented 3 years ago

I have a POJO with an array of Comparable types (DayOfWeek in my case). This is logically a set, an it is therefore represented as such in the object. When serialized using an unordered set (such as a HashSet), the elements are written to JSON in the set's arbitrary iteration order, and not in a logical sorted order. This makes the data more annoying to work with: it's harder to see what elements are present at a visual glance, and any tests on the JSON structure itself need to make sure they're ignoring the ordering of the resulting array.

Set<DayOfWeek> unorderedSet = new HashSet<>(EnumSet.allOf(DayOfWeek.class));
JsonMapper jsonMapper = JsonMapper.builder().build();
System.out.println(jsonMapper.writeValueAsString(unorderedSet));
// ["WEDNESDAY","MONDAY","THURSDAY","SUNDAY","FRIDAY","TUESDAY","SATURDAY"]

Set<DayOfWeek> orderedSet = EnumSet.allOf(DayOfWeek.class);
System.out.println(jsonMapper.writeValueAsString(orderedSet));
// ["MONDAY","TUESDAY","WEDNESDAY","THURSDAY","FRIDAY","SATURDAY","SUNDAY"]

What I would like is the ability to use the natural order of the Set elements when outputting the array to JSON, i.e. something equivalent to ORDER_MAP_ENTRIES_BY_KEYS, but for collection entries, not map entries. In a perfect world, I guess it would be nice to be able to specify it globally for the ObjectMapper, as well as locally per property, but either would work. null values in the set should ideally be supported as well, even though they're not comparable, though this isn't a showstopper for my use case.

This might be a separate issue, but I'd like to be able to the inverse as well: when deserializing from a JSON array, produce a set with the desired order (e.g. with a LinkedHashSet).

Workaround

A simple custom Converter can be created to sort the elements on (de)serialization:

private static class MyPojo {
    // Constructors, getters, setters

    @JsonSerialize(converter = OrderedSetConverter.class)
    @JsonDeserialize(converter = OrderedSetConverter.class)
    private Set<DayOfWeek> daysOfWeek;
}

private static class OrderedSetConverter extends StdConverter<Set<DayOfWeek>, Set<DayOfWeek>> {
    @Override
    public Set<DayOfWeek> convert(Set<DayOfWeek> value) {
        return value == null ? null : value.stream()
                .sorted(Comparator.nullsLast(Comparator.naturalOrder()))
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }
}

There's probably a way to make it generic on <E extends Comparable<? super E>>, but the naive approach ran into some Jackson type issues, and I haven't yet taken the time to dig deeper to figure out the proper Jackson way to handle it.

cowtowncoder commented 3 years ago

Ok, I can see why such a feature would be useful. My main concern strictly from API perspective is that I hope to minimize addition of type-specific main-level Deserialization-/Serialization-/MapperFeatures. But then again there is room in SerializationFeature, and none of alternatives (@JsonFormat, CoercionConfig or "config overrides") fits this any better.

So I'll keep this open if anyone wants to take a stab; addition of a SerializationFeature similar to ORDER_MAP_ENTRIES_BY_KEYS (ORDER_SETS) would be acceptable. And then there probably should be matching JsonFormat.Feature.WRITE_SORTED_MAP_ENTRIES counterpart too, for per-property definition.

sschuberth commented 1 year ago

Is there currently also a way to make the OrderedSetConverter (in this example) take a Comparator if the element class itself does not implement Comparable? It would be cool if OrderedSetConverter could take a constructor argument for that, which I could somehow specify as part of the @JsonSerialize annotation.

cowtowncoder commented 1 year ago

OrderedSetConverter is just constructed via zero-arg constructor; there is no mechanism to inject anything. So it would have to implement/infer logic without much help from Jackson.