Skip to content
Advertisement

Jackson mixin is ignored on serialization and deserialization

I need to be able to create a Java POJO from a JSON object when I only have an interface that can’t be changed. I’m hoping that Mixins can help make this possible. I created a Mixin that hopefully will work but can’t get Jackson to use it.

It appears that Jackson is ignoring the Mixin I am defining for both an Interface and an Implementation. The test failures are what I would expect without the Mixin added to the ObjectMapper.

Below is the simplest example that shows the problem. The classes are each in their own package. The real uses case is much more complex, including Lists of interfaces. I am using Jackson 2.10.3.

Any suggestions on what I’m doing wrong?

Timothy

What doesn’t work

The interface reader test fails with InvalidDefinitionException: Cannot construct instance of model.Level4 (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information

Of secondary importance, the Mixin defines a new label (nameTest) for the name field which should be reflected in the output from writeValueAsString. It outputs the field with the original value for the label (name).

Interface

public interface Level4 {
    public Long getId();    
    public void setId(Long id);    
    public String getName();    
    public void setName(String name);
}

Implementation

public class Level4Impl implements Level4 {
    private Long id;
    private String name;

    @Override
    public Long getId() {
        return id;
    }

    @Override
    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }
}

Mixin

public abstract class Level4Mixin {
    public Level4Mixin(
        @JsonProperty("id") Long id,
        @JsonProperty("nameTest") String name) { }
}

Unit Test

class Level4MixinTest {
    private ObjectMapper mapper;

    @BeforeEach
    void setUp() throws Exception {
        mapper = new ObjectMapper();
        mapper.addMixIn(Level4.class, Level4Mixin.class);
        mapper.addMixIn(Level4Impl.class, Level4Mixin.class);
    }

    @Test
    void test_InterfaceWrite() throws JsonProcessingException {
        Level4 lvl4 = new Level4Impl();
        lvl4.setId(1L);
        lvl4.setName("test");
        String json = mapper.writeValueAsString(lvl4);
        assertNotNull(json);
        assertTrue(json.contains("nameTest"));
    }

    @Test
    void test_InterfaceRead() throws JsonProcessingException {
        String json = "{"id":1,"nameTest":"test"}";
        assertDoesNotThrow(() -> {
            Level4 parsed = mapper.readValue(json, Level4.class);
            assertNotNull(parsed);
        });
    }

    @Test
    void test_ImplWrite() throws JsonProcessingException {
        Level4Impl lvl4 = new Level4Impl();
        lvl4.setId(1L);
        lvl4.setName("test");
        String json = mapper.writeValueAsString(lvl4);
        assertNotNull(json);
        assertTrue(json.contains("nameTest"));
    }

    @Test
    void test_ImplRead() {
        String json = "{"id":1,"nameTest":"test"}";
        assertDoesNotThrow(() -> {
            Level4Impl parsed = mapper.readValue(json, Level4Impl.class);
            assertNotNull(parsed);
        });
    }
}

Advertisement

Answer

First of all you have to let Jackson know which subclass of your interface it should instantiate. You do it by adding @JsonTypeInfo and/or @JsonSubTypes annotations to your mix-in class. For single subclass the following would suffice:

@JsonTypeInfo(use = Id.NAME, defaultImpl = Level4Impl.class)
public abstract class Level4Mixin {
}

For multiple sub-classes it will a bit more complex and will require additional field in JSON payload that will identify concrete type. See Jackson Polymorphic Deserialization for details. Also worth mentioning that adding type info will cause type ID field to be written to JSON. JFYI.

Adding new label would be as trivial as adding a pair of getter and setter for desired property. Obviously original name field will be written to JSON too in this case. To change that you may want to place @JsonIgnore on getter in subclass or in mix-in. In latter case name will be ignored for all sub-classes.

Last note: in this case you should register your mix-in with super-type only.

Here are the changes to your classes that satisfy your tests:

Level4Impl

public class Level4Impl implements Level4 {

    private Long id;
    private String name;

    @Override
    public Long getId() {
        return id;
    }

    @Override
    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    public String getNameTest() {
        return name;
    }

    public void setNameTest(String name) {
        this.name = name;
    }

}

Mixin

@JsonTypeInfo(use = Id.NAME, defaultImpl = Level4Impl.class)
public interface Level4Mixin {

    @JsonIgnore
    String getName();
}

Level4MixinTest change

@BeforeEach
void setUp() throws Exception {
    mapper = new ObjectMapper();
    mapper.addMixIn(Level4.class, Level4Mixin.class);
    // remove 
    //mapper.addMixIn(Level4Impl.class, Level4Mixin.class);
}
User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement