简体   繁体   中英

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);
        });
    }
}

For adding properties to an object when that object is serialized you can use @JsonAppend . For example:

@JsonAppend(attrs = {@JsonAppend.Attr(value = "nameTest")})
public class Level4Mixin {}

And the test:

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

@Test
void test_ImplWrite() throws JsonProcessingException {
    Level4Impl lvl4 = new Level4Impl();
    lvl4.setId(1L);
    lvl4.setName("test");

    String json = mapper.writerFor(Level4Impl.class)
            .withAttribute("nameTest", "myValue")
            .writeValueAsString(lvl4);

    assertNotNull(json);
    assertTrue(json.contains("nameTest"));
    assertTrue(json.contains("myValue"));
}

The same works for test_InterfaceWrite .

The tests to deserialize a json into an object are not clear:

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

The class Level4Impl does not have the property nameTest so the deserialization fails. If you don't want to throw the exception you can configure the ObjectMapper to don't fail on unknown properties. For example:

@Test
void test_ImplRead() {
    String json = "{\"id\":1,\"nameTest\":\"test\"}";
    assertDoesNotThrow(() -> {
        Level4Impl parsed = new ObjectMapper()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .readValue(json, Level4Impl.class);
        assertNotNull(parsed);
    });
}

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);
}

In case you can't make it work by default (which was my case), try to modify existing MappingJackson2HttpMessageConverter converter, eg do it this way:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.stream()
            .filter(c -> c instanceof MappingJackson2HttpMessageConverter)
            .forEach(c -> {
                MappingJackson2HttpMessageConverter converter = (MappingJackson2HttpMessageConverter) c;
                converter.getObjectMapper().addMixIn(APIResponse.class, MixInAPIResponse.class);
            });
}

Where MixInAPIResponse is your configured MixIn class for target class ApiResponse .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM