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

Default typing adds typing info to collections even with `@JsonTypeInfo(use = Id.NONE)` override #1391

Open jmax01 opened 8 years ago

jmax01 commented 8 years ago

Version: Jackson 2.8.2 JDK: 1.8.0_60

It appears that enableDefaultTypingAsProperty adds type info to collections regardless if @JsonTypeInfo(use = Id.NONE) is specified on the property.

 ({"treeSetStrings":["java.util.TreeSet",[]],"immutableSortedSetStrings":["com.google.common.collect.EmptyImmutableSortedSet",[]]})
import static org.junit.Assert.*;

import java.util.SortedSet;
import java.util.TreeSet;

import org.junit.Test;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.google.common.collect.ImmutableSortedSet;

@SuppressWarnings("javadoc")
public class ObjectAndNonConcreteCollectionsTest {

    final static ObjectMapper WITH_OBJECT_AND_NON_CONCRETE = new ObjectMapper().enableDefaultTypingAsProperty(
            DefaultTyping.OBJECT_AND_NON_CONCRETE, "$type");

    final static ObjectMapper PLAIN_OBJECT_MAPPER = new ObjectMapper();

    static class MyClass {

        private final SortedSet<String> treeSetStrings = new TreeSet<>();

        private final SortedSet<String> immutableSortedSetStrings = ImmutableSortedSet.of();

        @JsonTypeInfo(use = Id.NONE)
        public SortedSet<String> getTreeSetStrings() {
            return this.treeSetStrings;
        }

        @JsonTypeInfo(use = Id.NONE)
        public SortedSet<String> getImmutableSortedSetStrings() {
            return this.immutableSortedSetStrings;
        }
    }

    @Test
    public void testCollectionTyping() throws JsonProcessingException {

        final MyClass toSerialize = new MyClass();

        final String fromPlainObjectMapper = PLAIN_OBJECT_MAPPER.writeValueAsString(toSerialize);
        System.out.println("\nfrom plain ObjectMapper:\n" + fromPlainObjectMapper);

        final String fromObjectMapperWithDefaultTyping = WITH_OBJECT_AND_NON_CONCRETE.writeValueAsString(toSerialize);
        System.out.println("\nfrom ObjectMapper with default typing:\n" + fromObjectMapperWithDefaultTyping);

        assertEquals(fromPlainObjectMapper, fromObjectMapperWithDefaultTyping);

    }

}
cowtowncoder commented 8 years ago

Interesting. I can reproduce this as per above.

I think the problem comes from difference between handling of @JsonTypeInfo, and default typing:

  1. @JsonTypeInfo has special handling for structured types, as there's no way to indicate how it should apply to contents (in retrospect, it probably should have simple contents= property to allow separate definition), and so Jackson does same as JAXB -- it only applies to elements/values of arrays, Collections and Maps (and nowadays, referential types like Optional); but for non-structured types, value itself
  2. Default typing is separately considered both for Collection and its elements

So what I think happens here is that @JsonTypeInfo only affects handling of elements in Collection, but not Collection, thereby leaving default typing in effect.

Now... there are couple of ways in which this could be handled, perhaps. For example:

  1. Consider use of Id.NONE a special case that prevents use of type info for both structured value and its elements -- that seems a likely intent
  2. Add a new property or two for @JsonTypeInfo to allow specifying that it should apply to value itself, not elements, even for structured types. This could either be a boolean, or enumeration -- enum allowing more choices (value, elements, both).

Of these, (2) would be safer, and potentially more usable, although it still would not allow defining type info settings for both container and values separately. But it could only be added in next minor version (2.9). (1) on the other hand could be added in a patch version; most likely 2.8. It would however be slightly higher risk; theoretically it could break existing code I guess I am leaning slightly on (2), as it would be cleaner.

Now... since this is not as simple a fix as I first hoped, a work-around you may want is to specify custom handler for deciding where/when to apply default typing:

public ObjectMapper setDefaultTyping(TypeResolverBuilder<?> typer) { ... }

so, you need to call that instead of enableDefaultTyping(). Class `DefaultTypeResolverBuilder (within ObjectMapper) shows how to implement logic.

jmax01 commented 8 years ago

Deserialization actually throws an exception:

import static org.junit.Assert.*;

import java.io.IOException;
import java.util.SortedSet;
import java.util.TreeSet;

import org.junit.Test;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.guava.GuavaModule;
import com.google.common.collect.ImmutableSortedSet;

@SuppressWarnings("javadoc")
public class ObjectAndNonConcreteCollectionsTest {

    final static ObjectMapper WITH_OBJECT_AND_NON_CONCRETE = new ObjectMapper().enable(
            SerializationFeature.INDENT_OUTPUT)
        .enableDefaultTypingAsProperty(DefaultTyping.OBJECT_AND_NON_CONCRETE, "$type")
        .registerModule(new GuavaModule());

    final static ObjectMapper PLAIN_OBJECT_MAPPER = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT)
        .registerModule(new GuavaModule());

    static class MyClass {

        private SortedSet<String> treeSetStrings;

        private SortedSet<String> immutableSortedSetStrings;

        @JsonTypeInfo(use = Id.NONE)
        public SortedSet<String> getTreeSetStrings() {
            return this.treeSetStrings;
        }

        @JsonTypeInfo(use = Id.NONE)
        public SortedSet<String> getImmutableSortedSetStrings() {
            return this.immutableSortedSetStrings;
        }

        public void setImmutableSortedSetStrings(ImmutableSortedSet<String> immutableSortedSetStrings) {
            this.immutableSortedSetStrings = immutableSortedSetStrings;
        }

        public void setTreeSetStrings(TreeSet<String> treeSetStrings) {
            this.treeSetStrings = treeSetStrings;
        }

    }

    @Test
    public void testCollectionTypingSerialization() throws IOException {

        final MyClass toSerialize = new MyClass();
        toSerialize.setImmutableSortedSetStrings(ImmutableSortedSet.of());
        toSerialize.setTreeSetStrings(new TreeSet<>());

        final String fromPlainObjectMapper = PLAIN_OBJECT_MAPPER.writeValueAsString(toSerialize);
        System.out.println("\nfrom plain ObjectMapper:\n" + fromPlainObjectMapper);

        final String fromObjectMapperWithDefaultTyping = WITH_OBJECT_AND_NON_CONCRETE.writeValueAsString(toSerialize);
        System.out.println("\nfrom ObjectMapper with default typing:\n" + fromObjectMapperWithDefaultTyping);

        assertEquals(fromPlainObjectMapper, fromObjectMapperWithDefaultTyping);

    }

    @Test
    public void testCollectionTypingDeserialization() throws IOException {

        final MyClass toSerialize = new MyClass();
        toSerialize.setImmutableSortedSetStrings(ImmutableSortedSet.of());
        toSerialize.setTreeSetStrings(new TreeSet<>());

        final String fromPlainObjectMapper = PLAIN_OBJECT_MAPPER.writeValueAsString(toSerialize);
        System.out.println("\nfrom plain ObjectMapper:\n" + fromPlainObjectMapper);

        final String fromObjectMapperWithDefaultTyping = WITH_OBJECT_AND_NON_CONCRETE.writeValueAsString(toSerialize);
        System.out.println("\nfrom ObjectMapper with default typing:\n" + fromObjectMapperWithDefaultTyping);

        //Works
        final MyClass roundTripPlainObjectMapper = PLAIN_OBJECT_MAPPER.readValue(fromPlainObjectMapper, MyClass.class);

        // Fails with:
        // com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.lang.String out of
        // START_ARRAY token
        // at [Source: {
        // "treeSetStrings" : [ "java.util.TreeSet", [ ] ],
        // "immutableSortedSetStrings" : [ "com.google.common.collect.EmptyImmutableSortedSet", [ ] ]
        // }; line: 2, column: 45] (through reference chain: MyClass["treeSetStrings"]->java.util.TreeSet[1])

        final MyClass roundObjectMapperWithDefaultTyping = WITH_OBJECT_AND_NON_CONCRETE.readValue(
                fromObjectMapperWithDefaultTyping, MyClass.class);

    }

}