5

I'd like to be able to deserialize an UnmodifiableSet with default typing enabled. To do this I have created an UnmodifiableSetMixin as shown below:

NOTE: You can find a minimal project with all the source code to reproduce this issue at https://github.com/rwinch/jackson-unmodifiableset-mixin

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

import java.util.Set;

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
public abstract class UnmodifiableSetMixin {

    @JsonCreator
    public UnmodifiableSetMixin(Set<?> s) {}
}

I then try to use this to deserialize an empty set.

public class UnmodifiableSetMixinTest {
    static final String EXPECTED_JSON = "[\"java.util.Collections$UnmodifiableSet\",[]]";

    ObjectMapper mapper;

    @Before
    public void setup() {
        mapper = new ObjectMapper();
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        mapper.addMixIn(Collections.unmodifiableSet(Collections.<String>emptySet()).getClass(), UnmodifiableSetMixin.class);
    }

    @Test
    @SuppressWarnings("unchecked")
    public void read() throws Exception {
        Set<String> foo = mapper.readValue(EXPECTED_JSON, Set.class);
        assertThat(foo).isEmpty();
    }
}

The test passes with Jackson 2.6, but fails using Jackson 2.7+ with the following stack trace:

java.lang.IllegalStateException: No default constructor for [collection type; class java.util.Collections$UnmodifiableSet, contains [simple type, class java.lang.Object]]
    at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createUsingDefault(StdValueInstantiator.java:240)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:249)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:26)
    at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:110)
    at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromArray(AsArrayTypeDeserializer.java:50)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserializeWithType(CollectionDeserializer.java:310)
    at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:42)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3788)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2779)
    at sample.UnmodifiableSetMixinTest.read(UnmodifiableSetMixinTest.java:36)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

Can anyone help me fix the test for Jackson 2.7+ (I'd like it to work for Jackson 2.8.3)?

Rob Winch
  • 21,440
  • 2
  • 59
  • 76
  • 1
    Just curios if you can change from `UnmodifiableSetMixin(Set> s) {}` to `public UnmodifiableSetMixin(Set> s) {}` – Manuel Jordan Sep 29 '16 at 17:50
  • Thanks for the response. Unfortuantely, the test still fails with the same error message when the mixin has a public constructor. I have updated the sample code on SO and in the github repo to reflect this – Rob Winch Sep 29 '16 at 18:21
  • Just to play, what happens if you **add** a new `public UnmodifiableSetMixin() {}` (non-args constructor). You should have two. – Manuel Jordan Sep 29 '16 at 18:25
  • That fails because `java.util.Collections$UnmodifiableSet` doesn't have a default constructor. If you would like to try these ideas yourself, you can clone the very simple project I provided at https://github.com/rwinch/jackson-unmodifiableset-mixin – Rob Winch Sep 29 '16 at 18:28
  • Sorry just noticed the add the constructor. This fails in the exact same way. – Rob Winch Sep 29 '16 at 18:37
  • Which Jackson version? Have you tried the latest 2.7 one (2.7.8)? – StaxMan Sep 29 '16 at 22:29
  • I've tried 2.8.3 and it does not work – Rob Winch Sep 30 '16 at 00:44

2 Answers2

4

It turns out that this is a regression in Jackson. I created https://github.com/FasterXML/jackson-databind/issues/1392 which acknowledges the bug.

A workaround that uses a custom deserializer was provided to me via #4078. For example:

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonDeserialize(using = UnmodifiableSetDeserializer.class)
public abstract class UnmodifiableSetMixin {
    @JsonCreator
    public UnmodifiableSetMixin(Set<?> s) {}
}

public class UnmodifiableSetDeserializer extends JsonDeserializer<Set> {

    @Override
    public Set deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        ObjectMapper mapper = (ObjectMapper) jp.getCodec();
        JsonNode node = mapper.readTree(jp);
        Set<Object> resultSet = new HashSet<Object>();
        if (node != null) {
            if (node instanceof ArrayNode) {
                ArrayNode arrayNode = (ArrayNode) node;
                Iterator<JsonNode> nodeIterator = arrayNode.iterator();
                while (nodeIterator.hasNext()) {
                    JsonNode elementNode = nodeIterator.next();
                    resultSet.add(mapper.readValue(elementNode.toString(), Object.class));
                }
            } else {
                resultSet.add(mapper.readValue(node.toString(), Object.class));
            }
        }
        return Collections.unmodifiableSet(resultSet);
    }
}
Rob Winch
  • 21,440
  • 2
  • 59
  • 76
0

For mix-in to work, there must be a one-argument (*) constructor on that internal class; if not, mix-in does not get associated.

But do you absolutely require use of this JDK internal class? If not, you may be able add mapping to indicate that you want to use one of standard Collection implementations instead on deserialization using SimpleModule.addAbstractTypeMapping(abstractType, concreteType), registering that module.

(*) EDIT At first I said "zero-argument" constructor; but constructor in question does take one argument.

StaxMan
  • 113,358
  • 34
  • 211
  • 239
  • Can you expand on zero argument constructor? Do you mean the class being deserialized or the mixin? Neither makes sense since this works in Jackson < 2.7 Can you show me what code needs to be changed to fix this. I must be able to deserialize UnmodifiableSet because the class must remain passive. Note see the linked github project to run the actual code – Rob Winch Sep 30 '16 at 00:48
  • First; I should have said "one-argument ctor", not zero. And then I mean that the "target" class (one for which mix-in would conceptually augment) must have things on which annotations from mix-in would attach. Mix-ins do not cause any bytecode changes, so if target has no constructor, mix-ins annotations would not be used. – StaxMan Sep 30 '16 at 17:57
  • The target (UnmodifiableSet) does have a single argument constructor that takes a Set (it is part of the JDK distribution so you can see for yourself). – Rob Winch Sep 30 '16 at 18:05
  • @RobWinch ok then that should not be the problem here. I just mentioned since that could have been an issue -- although obviously should not differ b/w jackson versions on same JDK – StaxMan Sep 30 '16 at 20:12